diff --git a/.cursor/rules/coding_standards.mdc b/.cursor/rules/coding_standards.mdc
deleted file mode 100644
index 7ece800..0000000
--- a/.cursor/rules/coding_standards.mdc
+++ /dev/null
@@ -1,124 +0,0 @@
----
-description:
-globs:
-alwaysApply: true
----
-# Coding Standards & Best Practices
-
-This document outlines the core coding standards, best practices, and quality control procedures for the codebase.
-
-## Type Hints
-
-1. **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 types with Uppercase first letter (Dict[], List[], etc.)
-
-2. **StrEnum**
- - Import from `pipelex.types`:
- ```python
- from pipelex.types import StrEnum
- ```
-
-## BaseModel Standards
-
-- Respect Pydantic v2 standards
-- Keep models focused and single-purpose
-- Use descriptive field names
-- Use type hints for all fields
-- Document complex validations
-- Use Optional[] for nullable fields
-- Use Field(default_factory=...) for mutable defaults
-
-## Factory Pattern
-
-- Use Factory Pattern for object creation when dealing with multiple implementations
-
-## 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.
- """
- ```
-
-## Error Handling
-
-1. **Graceful Error Handling**
- - Use try/except blocks with specific exceptions
- - Convert third-party exceptions to custom ones
- ```python
- try:
- from fal_client import AsyncClient as FalAsyncClient
- except ImportError as exc:
- raise MissingDependencyError(
- "fal-client", "fal",
- "The fal-client SDK is required to use FAL models."
- ) from exc
- ```
-
-## 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 imgg or ocr)) 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 to`, `make test-ocr`, `make tg`, or `make test-imgg` - these use costly inference.
-
-## Pipelines
-
-- All pipeline definitions go in `your/path/to/pipelex/config/folder/pipelines/`
-- Always validate pipelines after creation/edit with `make validate`.
- Iterate if there are errors.
-
-## Project Structure
-
-- **Pipelines**: `your/path/to/pipelex/config/folder/pipelines/`
-- **Tests**: `tests/` directory
-- **Documentation**: `docs/` directory
\ No newline at end of file
diff --git a/.cursor/rules/commands.mdc b/.cursor/rules/commands.mdc
new file mode 100644
index 0000000..6c017d1
--- /dev/null
+++ b/.cursor/rules/commands.mdc
@@ -0,0 +1,8 @@
+---
+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
index 31461aa..1a16650 100644
--- a/.cursor/rules/docs.mdc
+++ b/.cursor/rules/docs.mdc
@@ -1,8 +1,16 @@
---
-description:
-globs: docs/**/*.md
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
index 00a2f8c..a21831a 100644
--- a/.cursor/rules/llms.mdc
+++ b/.cursor/rules/llms.mdc
@@ -1,35 +1,36 @@
---
-globs: *.plx,*.toml
alwaysApply: false
+description: LLM configuration and usage guidelines
+globs:
+- '*.plx'
+- '*.toml'
---
# Rules to choose LLM models used in PipeLLMs.
-## LLM Handles
+## LLM Configuration System
-In order to use it in a pipe, an LLM is referenced by its llm_handle and possibly by an llm_preset.
-Both llm_handles and llm_presets are defined in this toml config file: [base_llm_deck.toml](mdc:pipelex/libraries/llm_deck/base_llm_deck.toml)
+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/`:
-## LLM Handles
+- **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`
-An llm_handle matches the handle (an id of sorts) with the full specification of the LLM to use, i.e.:
-- llm_name
-- llm_version
-- llm_platform_choice
+## LLM Handles
-The declaration of llm_handles looks like this in toml syntax:
-```toml
-[llm_handles]
-gpt-4o-2024-11-20 = { llm_name = "gpt-4o", llm_version = "2024-11-20" }
-```
+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:
-In mosty cases, we only want to use version "latest" and llm_platform_choice "default" in which case the declaration is simply a match of the llm_handle to the llm_name, like this:
```toml
-best-claude = "claude-4-opus"
-best-gemini = "gemini-2.5-pro"
-best-mistral = "mistral-large"
+[aliases]
+base-claude = "claude-4.5-sonnet"
+base-gpt = "gpt-5"
+base-gemini = "gemini-2.5-flash"
+base-mistral = "mistral-medium"
```
-And of course, llm_handles are automatically assigned for all models by their name, with version "latest" and llm_platform_choice "default".
+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
@@ -38,10 +39,10 @@ Here is an example of using an llm_handle to specify which LLM to use in a PipeL
```plx
[pipe.hello_world]
type = "PipeLLM"
-definition = "Write text about Hello World."
+description = "Write text about Hello World."
output = "Text"
-llm = { llm_handle = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
-prompt_template = """
+model = { model = "gpt-5", temperature = 0.9 }
+prompt = """
Write a haiku about Hello World.
"""
```
@@ -54,8 +55,8 @@ Presets are meant to record the choice of an llm with its hyper parameters (temp
Examples:
```toml
-llm_to_reason = { llm_handle = "o4-mini", temperature = 1, max_tokens = "auto" }
-llm_to_extract_invoice = { llm_handle = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
+llm_to_reason = { 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:
@@ -63,11 +64,11 @@ The interest is that these presets can be used to set the LLM choice in a PipeLL
```plx
[pipe.extract_invoice]
type = "PipeLLM"
-definition = "Extract invoice information from an invoice text transcript"
+description = "Extract invoice information from an invoice text transcript"
inputs = { invoice_text = "InvoiceText" }
output = "Invoice"
-llm = "llm_to_extract_invoice"
-prompt_template = """
+model = "llm_to_extract_invoice"
+prompt = """
Extract invoice information from this invoice:
The category of this invoice is: $invoice_details.category.
@@ -76,8 +77,9 @@ The category of this invoice is: $invoice_details.category.
"""
```
-The setting here `llm = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
+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 in [overrides.toml](your/path/to/pipelex/config/folder/llm_deck/overrides.toml).
+You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
+
diff --git a/.cursor/rules/pipelex.mdc b/.cursor/rules/pipelex.mdc
deleted file mode 100644
index 4c0f292..0000000
--- a/.cursor/rules/pipelex.mdc
+++ /dev/null
@@ -1,520 +0,0 @@
----
-alwaysApply: true
----
-# Pipeline Guide
-
-- Always first write your "plan" in natural langage, then transcribe it in pipelex.
-- You should ALWAYS RUN the terminal command `make validate` when you are writing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
-- Please use POSIX standard for files. (enmpty lines, no trailing whitespaces, etc.)
-
-# Pipeline Structure Guide
-
-## 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 structures
-- Use descriptive names in `snake_case`
-
-## Pipeline File Structure
-A pipeline file has three main sections:
-1. Domain statement
-2. Concept definitions
-3. Pipe definitions
-
-### Domain Statement
-```plx
-domain = "domain_name"
-definition = "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
-```plx
-[concept]
-ConceptName = "Description of the concept" # Should be the same name as the Structure ClassName you want to output
-```
-
-Important Rules:
-- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story")
-- Avoid adjectives (no "LargeText", use "Text")
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number)
-yes
-### Pipe Definitions
-
-## Pipe Base Structure
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-definition = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
-
-DO NOT WRITE:
-```plx
-[pipe.your_pipe_name]
-type = "pipe_sequence"
-```
-
-But it should be:
-
-```plx
-[pipe.your_pipe_name]
-type = "PipeSequence"
-definition = "....."
-```
-
-The pipes will all have at least this base structure.
-- `inputs`: Dictionnary of key behing the variable used in the prompts, and the value behing the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if pipeSequence) or of the conditionnal pipes (if pipeCondition).
-So If you have this error:
-`StaticValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['ocr_input']'``
-That means that the pipe validate_expense is missing the input `ocr_input` 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:
-
-# Structured Models Rules
-
-## Model Location and Registration
-
-- Create models for structured generations related to "some_domain" in `pipelex_libraries/pipelines/.py`
-- Models must inherit from `StructuredContent` or appropriate content type
-
-## Model Structure
-
-Concepts and their structure classes are meant to indicate an idea.
-A Concept MUST NEVER be a plural noun and you should never create a SomeConceptList: lists and arrays are implicitly handled by Pipelex according to the context. Just define SomeConcept.
-
-```python
-from datetime import datetime
-from typing import List, Optional
-from pydantic import Field
-
-from pipelex.core.stuffs.stuff_content import StructuredContent
-
-# IMPORTANT: THE CLASS MUST BE A SUBCLASS OF StructuredContent
-class YourModel(StructuredContent): # Always be a subclass of StructuredContent
- # Required fields
- field1: str
- field2: int
-
- # Optional fields with defaults
- field3: Optional[str] = Field(None, "Description of field3")
- field4: List[str] = Field(default_factory=list)
-
- # Date fields should remove timezone
- date_field: Optional[datetime] = None
-```
-## Usage
-
-Structures are meant to indicate what class to use for a particular Concept. In general they use the same name as the concept.
-
-Structure classes defined within `pipelex_libraries/pipelines/` are automatically loaded into the class_registry when setting up Pipelex, no need to do it manually.
-
-
-## Best Practices for structures
-
-- Respect Pydantic v2 standards
-- Use type hints for all fields
-- Use `Field` declaration and write the description
-
-
-## Pipe Controllers and Pipe Operator
-
-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
- - `PipeBatch` - For running pipes in Batch over a ListContent
-
-2. **Operators** - For specific tasks:
- - `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeOcr` - OCR Pipe
- - `PipeImgGen` - Generate Images
- - `PipeFunc` - For running classic python scripts
-
-# PipeSequence Guide
-
-## Purpose
-PipeSequence executes multiple pipes in a defined order, where each step can use results from previous steps.
-
-## Basic Structure
-```plx
-[pipe.your_sequence_name]
-type = "PipeSequence"
-definition = "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.
-
-## Usage in PLX Configuration
-
-### Basic Usage with Direct Expression
-
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression = "input_data.category"
-
-[pipe.conditional_operation.pipe_map]
-small = "process_small"
-medium = "process_medium"
-large = "process_large"
-```
-or
-```plx
-[pipe.conditional_operation]
-type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
-inputs = { input_data = "CategoryInput" }
-output = "native.Text"
-expression_template = "{{ input_data.category }}" # Jinja2 code
-
-[pipe.conditional_operation.pipe_map]
-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)
-- `pipe_map`: 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`, ..) is the name of the pipce to trigger
-
-# PipeBatch Controller
-
-The PipeBatch controller allows you to apply a pipe operation to each element in a list of inputs in parallele. It is created via a PipeSequence.
-
-## Usage in PLX Configuration
-
-```plx
-[pipe.sequence_with_batch]
-type = "PipeSequence"
-definition = "A Sequence of pipes"
-inputs = { input_data = "ConceptName" }
-output = "OutputConceptName"
-steps = [
- { pipe = "pipe_to_apply", batch_over = "input_list", batch_as = "current_item", result = "batch_results" }
-]
-```
-
-## Key Parameters
-
-- `pipe`: The pipe operation to apply to each element in the batch
-- `batch_over`: The name of the list in the context to iterate over
-- `batch_as`: The name to use for the current element in the pipe's context
-- `result`: Where to store the results of the batch operation
-
-# PipeLLM Guide
-
-## Purpose
-
-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"
-definition = "Write a short story"
-output = "Text"
-prompt_template = """
-Write a short story about a programmer.
-"""
-```
-
-### Structured Data Extraction
-```plx
-[pipe.extract_info]
-type = "PipeLLM"
-definition = "Extract information"
-inputs = { text = "Text" }
-output = "PersonInfo"
-prompt_template = """
-Extract person information from this text:
-@text
-"""
-```
-
-### System Prompts
-Add system-level instructions:
-```plx
-[pipe.expert_analysis]
-type = "PipeLLM"
-definition = "Expert analysis"
-output = "Analysis"
-system_prompt = "You are a data analysis expert"
-prompt_template = "Analyze this data"
-```
-
-### Multiple Outputs
-Generate multiple results:
-```plx
-[pipe.generate_ideas]
-type = "PipeLLM"
-definition = "Generate ideas"
-output = "Idea"
-nb_output = 3 # Generate exactly 3 ideas
-# OR
-multiple_output = true # Let the LLM decide how many to generate
-```
-
-### Vision Tasks
-Process images with VLMs:
-```plx
-[pipe.analyze_image]
-type = "PipeLLM"
-definition = "Analyze image"
-inputs = { image = "Image" } # `image` is the name of the stuff that contains the Image. If its in a stuff, you can add something like `{ "page.image": "Image" }
-output = "ImageAnalysis"
-prompt_template = "Describe what you see in this image"
-```
-
-# PipeOCR Guide
-
-## Purpose
-
-Extract text and images from an image or a PDF
-
-## Basic Usage
-
-### Simple Text Generation
-```plx
-[pipe.extract_info]
-type = "PipeOcr"
-definition = "extract the information"
-inputs = { ocr_input = "PDF" } # or { ocr_input = "Image" } if its an image. This is the only input
-output = "Page"
-```
-
-The input ALWAYS HAS TO BE `ocr_input` and the value is either of concept `Image` or `Pdf`.
-
-The output concept `Page` is a native concept, with the structure `PageContent`:
-It corresponds to 1 page. Therefore, the PipeOcr is outputing a `ListContent` of `Page`
-
-```python
-class TextAndImagesContent(StuffContent):
- text: Optional[TextContent]
- images: Optional[List[ImageContent]]
-
-class PageContent(StructuredContent): # CONCEPT IS "Page"
- text_and_images: TextAndImagesContent
- page_view: Optional[ImageContent] = 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.
-
-This rule explains how to write prompt templates in PipeLLM definitions.
-
-## Insert stuff inside a tagged block
-
-If the inserted text is supposedly 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_template = """
-Match the expense with its corresponding invoice:
-
-@expense
-
-@invoices
-"""
-```
-In this example, 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 doens't need to be explictly written in the prompt template.
-
-**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_template = """
-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.
-"""
-```
-
-Here, $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" included in an actual sentence.**
-
-# Example to execute a pipeline
-
-```python
-import asyncio
-
-from pipelex import pretty_print
-from pipelex.hub import get_pipeline_tracker, get_report_delegate
-from pipelex.pipelex import Pipelex
-from pipelex.pipeline.execute import execute_pipeline
-
-from pipelex_libraries.pipelines.examples.extract_gantt.gantt 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",
- input_memory={
- "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))
-
-# Display cost report (tokens used and cost)
-get_report_delegate().generate_report()
-# output results
-pretty_print(gantt_chart, title="Gantt Chart")
-get_pipeline_tracker().output_flowchart()
-```
-
-The input memory is a dictionary of key-value pairs, 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
-ImplicitMemory = 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.
-
-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",
- input_memory={
- "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",
- input_memory={
- "ocr_input": 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",
- input_memory={
- "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",
- input_memory={
- "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",
- input_memory={
- "text": load_text_from_path(path=text_path),
- "question": {
- "concept": "answer.Question",
- "content": question,
- },
- "client_instructions": client_instructions,
- },
- )
-```
-
-ALWAYS RUN `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-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/.cursor/rules/pytest.mdc b/.cursor/rules/pytest_standards.mdc
similarity index 51%
rename from .cursor/rules/pytest.mdc
rename to .cursor/rules/pytest_standards.mdc
index e93a05c..b1c2f11 100644
--- a/.cursor/rules/pytest.mdc
+++ b/.cursor/rules/pytest_standards.mdc
@@ -1,15 +1,18 @@
---
-description:
-globs: tests/**/*.py
alwaysApply: false
+description: Guidelines for writing unit tests
+globs:
+- tests/**/*.py
---
-These rules apply when writing unit tests.
-- Always use pytest
+# Writing unit tests
-## Test file structure
+## Unit test generalities
+
+NEVER USE unittest.mock or MagicMock. YOU MUST USE pytest-mock instead.
+
+### Test file structure
- Name test files with `test_` prefix
-- Use descriptive names that match the functionality being tested
- 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
@@ -20,23 +23,24 @@ These rules apply when writing unit tests.
- 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
+### Markers
Apply the appropriate markers:
- "llm: uses an LLM to generate text or objects"
-- "imgg: uses an image generation AI"
+- "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`.
-## Tips
+### Important rules
-- Never use the unittest.mock. Use pytest-mock
+- Never use the unittest.mock. Use pytest-mock.
-## Test Class Structure
+### Test Class Structure
-Always group the tests of a module into a test class:
+- Always group the tests of a module into a test class:
```python
@pytest.mark.llm
@@ -44,7 +48,7 @@ Always group the tests of a module into a test class:
@pytest.mark.asyncio(loop_scope="class")
class TestFooBar:
@pytest.mark.parametrize(
- "topic test_case_blueprint",
+ "topic, test_case_blueprint",
[
TestCases.CASE_1,
TestCases.CASE_2,
@@ -59,77 +63,13 @@ class TestFooBar:
# Test implementation
```
-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`.
-
-# Pipe tests
-
-## Required imports for pipe tests
-
-```python
-import pytest
-from pytest import FixtureRequest
-from pipelex import log, pretty_print
-from pipelex.core.stuffs.stuff_factory import StuffBlueprint, StuffFactory
-from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory
-from pipelex.hub import get_report_delegate
-from pipelex_libraries.pipelines.base_library.retrieve import RetrievedExcerpt
-from pipelex.config_pipelex import get_config
-
-from pipelex.core.pipe import PipeAbstract, update_job_metadata_for_pipe
-from pipelex.core.pipes.pipe_output import PipeOutput, PipeOutputType
-from pipelex.core.pipes.pipe_run_params import PipeRunParams
-from pipelex.core.pipes.pipe_run_params import PipeRunParams
-from pipelex.pipe_works.pipe_router_protocol import PipeRouterProtocol
-```
-
-## Pipe test implementation steps
-
-1. Create Stuff from blueprint:
-
-```python
-stuff = StuffFactory.make_stuff(
- concept_code="RetrievedExcerpt",
- domain="retrieve",
- content=RetrievedExcerpt(text="", justification="")
- name="retrieved_text",
-)
-```
-
-2. Create Working Memory:
-
-```python
-working_memory = WorkingMemoryFactory.make_from_single_stuff(stuff=stuff)
-```
-
-3. Run the pipe:
-
-```python
-pipe_output: PipeOutput = await pipe_router.run_pipe(
- pipe_code="pipe_name",
- pipe_run_params=PipeRunParamsFactory.make_run_params(),
- working_memory=working_memory,
- job_metadata=JobMetadata(),
-)
-```
-
-4. Log output and generate report:
-
-```python
-pretty_print(pipe_output, title=f"Pipe output")
-get_report_delegate().generate_report()
-```
-
-5. Basic assertions:
-
-```python
-assert pipe_output is not None
-assert pipe_output.working_memory is not None
-assert pipe_output.main_stuff is not None
-```
+- 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
+### Test Data Organization
-- If it's not already there, create a `test_data.py` file in the test directory
+- If it's not already there, create a `test_data.py` file in the proper test directory
- Define test cases using `StuffBlueprint`:
```python
@@ -145,7 +85,7 @@ class TestCases:
value="test_value"
)
- CASE_BLUEPRINTS: ClassVar[List[Tuple[str, str]]] = [ # topic, blueprint"
+ CASE_BLUEPRINTS: ClassVar[list[tuple[str, str]]] = [ # topic, blueprint"
("topic1", CASE_BLUEPRINT_1),
("topic2", CASE_BLUEPRINT_2),
]
@@ -156,11 +96,12 @@ Also note that we provide a topic for the test case, which is purely for conveni
## 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
+- 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
new file mode 100644
index 0000000..13d3c5a
--- /dev/null
+++ b/.cursor/rules/python_standards.mdc
@@ -0,0 +1,175 @@
+---
+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.
+
+## 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.debug("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
new file mode 100644
index 0000000..5f29c45
--- /dev/null
+++ b/.cursor/rules/run_pipelex.mdc
@@ -0,0 +1,231 @@
+---
+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",
+ input_memory={
+ "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
+ImplicitMemory = 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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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
index 575544a..4b4f058 100644
--- a/.cursor/rules/tdd.mdc
+++ b/.cursor/rules/tdd.mdc
@@ -1,7 +1,6 @@
---
-description:
-globs:
alwaysApply: false
+description: Guidelines for writing test-driven development code
---
# Test-Driven Development Guide
@@ -26,3 +25,4 @@ If the code needs refactoring, with the best practices [coding_standards.mdc](co
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
new file mode 100644
index 0000000..6e3339c
--- /dev/null
+++ b/.cursor/rules/write_pipelex.mdc
@@ -0,0 +1,837 @@
+---
+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 the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- 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)
+
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. 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:
+`StaticValidationError: 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:
+
+## 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"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, 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):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+nb_output = 3 # Generate exactly 3 ideas
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+multiple_output = true # Let the LLM decide how many to generate
+```
+
+### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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 dd04788..22b29d7 100644
--- a/.env.example
+++ b/.env.example
@@ -1,3 +1 @@
-PYRIGHT_PYTHON_FORCE_VERSION=1.1.398
-
PIPELEX_INFERENCE_API_KEY=
\ No newline at end of file
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 93859f7..4d612f7 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,73 +1,75 @@
-# Pipeline Guide
+
+## Guide to write or edit pipelines using the Pipelex language in .plx files
-- Always first write your "plan" in natural langage, then transcribe it in pipelex.
-- You should ALWAYS RUN the terminal command `make validate` when you are writing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
-- Please use POSIX standard for files. (enmpty lines, no trailing whitespaces, etc.)
+- Always first write your "plan" in natural language, then transcribe it in pipelex.
+- You should ALWAYS RUN the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-# Pipeline Structure Guide
-
-## Pipeline File Naming
+### 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 structures
+- Files must be `.py` for code defining the data structures
- Use descriptive names in `snake_case`
-## Pipeline File Structure
+### Pipeline File Outline
A pipeline file has three main sections:
1. Domain statement
2. Concept definitions
3. Pipe definitions
-### Domain Statement
+#### Domain Statement
```plx
domain = "domain_name"
-definition = "Description of the domain" # Optional
+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
+#### 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" # Should be the same name as the Structure ClassName you want to output
+ConceptName = "Description of the concept"
```
-Important Rules:
+**Naming Rules:**
- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story")
-- Avoid adjectives (no "LargeText", use "Text")
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number)
-yes
-### Pipe Definitions
+- 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)
-## Pipe Base Structure
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. Use these directly or refine them when appropriate.
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-definition = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
+**Refining Native Concepts:**
+To create a concept that specializes a native concept without adding fields:
-DO NOT WRITE:
```plx
-[pipe.your_pipe_name]
-type = "pipe_sequence"
+[concept.Landscape]
+description = "A scenic outdoor photograph"
+refines = "Image"
```
-But it should be:
+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 = "PipeSequence"
-definition = "....."
+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 structure.
-- `inputs`: Dictionnary of key behing the variable used in the prompts, and the value behing the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if pipeSequence) or of the conditionnal pipes (if pipeCondition).
+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:
`StaticValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['ocr_input']'``
-That means that the pipe validate_expense is missing the input `ocr_input` because one of the subpipe is needing it.
+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:
@@ -81,53 +83,108 @@ inputs = {
- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-# Structured Models Rules
+### Structuring Models
-## Model Location and Registration
+Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-- Create models for structured generations related to "some_domain" in `pipelex_libraries/pipelines/.py`
-- Models must inherit from `StructuredContent` or appropriate content type
+#### Three Ways to Structure Concepts
-## Model Structure
+**1. No Structure Needed**
-Concepts and their structure classes are meant to indicate an idea.
-A Concept MUST NEVER be a plural noun and you should never create a SomeConceptList: lists and arrays are implicitly handled by Pipelex according to the context. Just define SomeConcept.
+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.
-```python
-from datetime import datetime
-from typing import List, Optional
-from pydantic import Field
+**2. Inline Structure Definition (RECOMMENDED for most cases)**
+
+For concepts with structured fields, define them inline using TOML syntax:
-from pipelex.core.stuffs.stuff_content import StructuredContent
+```plx
+[concept.Invoice]
+description = "A commercial document issued by a seller to a buyer"
+
+[concept.Invoice.structure]
+invoice_number = "The unique invoice identifier"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, default_value = "default" }
+```
-# IMPORTANT: THE CLASS MUST BE A SUBCLASS OF StructuredContent
-class YourModel(StructuredContent): # Always be a subclass of StructuredContent
- # Required fields
- field1: str
- field2: int
+**3. Python StructuredContent Class (For Advanced Features)**
- # Optional fields with defaults
- field3: Optional[str] = Field(None, "Description of field3")
- field4: List[str] = Field(default_factory=list)
+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
- # Date fields should remove timezone
- date_field: Optional[datetime] = None
+```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
```
-## Usage
-Structures are meant to indicate what class to use for a particular Concept. In general they use the same name as the concept.
+**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
+
+#### Decision Rules for Agents
-Structure classes defined within `pipelex_libraries/pipelines/` are automatically loaded into the class_registry when setting up Pipelex, no need to do it manually.
+**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)
-## Best Practices for structures
+**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
-- Respect Pydantic v2 standards
-- Use type hints for all fields
-- Use `Field` declaration and write the description
+#### 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 Operator
+
+### Pipe Controllers and Pipe Operators
Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
@@ -135,24 +192,23 @@ Look at the Pipes we have in order to adapt it. Pipes are organized in two categ
- `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
- - `PipeBatch` - For running pipes in Batch over a ListContent
2. **Operators** - For specific tasks:
- `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeOcr` - OCR Pipe
+ - `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 Guide
+### PipeSequence controller
-## Purpose
-PipeSequence executes multiple pipes in a defined order, where each step can use results from previous steps.
+Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-## Basic Structure
+#### Basic Definition
```plx
[pipe.your_sequence_name]
type = "PipeSequence"
-definition = "Description of what this sequence does"
+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 = [
@@ -162,13 +218,13 @@ steps = [
]
```
-## Key Components
+#### 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
+#### Using PipeBatch in Steps
You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
@@ -189,23 +245,22 @@ steps = [
The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-# PipeCondition Controller
+### 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.
-## Usage in PLX Configuration
-
-### Basic Usage with Direct Expression
+#### Basic usage
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
@@ -214,205 +269,609 @@ or
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
```
-## Key Parameters
+#### 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)
-- `pipe_map`: 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`, ..) is the name of the pipce to trigger
-
-# PipeBatch Controller
-
-The PipeBatch controller allows you to apply a pipe operation to each element in a list of inputs in parallele. It is created via a PipeSequence.
-
-## Usage in PLX Configuration
+- `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.sequence_with_batch]
-type = "PipeSequence"
-definition = "A Sequence of pipes"
-inputs = { input_data = "ConceptName" }
-output = "OutputConceptName"
-steps = [
- { pipe = "pipe_to_apply", batch_over = "input_list", batch_as = "current_item", result = "batch_results" }
-]
-```
-
-## Key Parameters
-
-- `pipe`: The pipe operation to apply to each element in the batch
-- `batch_over`: The name of the list in the context to iterate over
-- `batch_as`: The name to use for the current element in the pipe's context
-- `result`: Where to store the results of the batch operation
+[pipe.strict_validation]
+type = "PipeCondition"
+description = "Validate with strict matching"
+inputs = { status = "Status" }
+output = "Text"
+expression = "status.value"
+default_outcome = "fail"
-# PipeLLM Guide
+[pipe.strict_validation.outcomes]
+approved = "process_approved"
+rejected = "process_rejected"
+```
-## Purpose
+### PipeLLM operator
PipeLLM is used to:
1. Generate text or objects with LLMs
2. Process images with Vision LLMs
-## Basic Usage
+#### Basic Usage
-### Simple Text Generation
+Simple Text Generation:
```plx
[pipe.write_story]
type = "PipeLLM"
-definition = "Write a short story"
+description = "Write a short story"
output = "Text"
-prompt_template = """
+prompt = """
Write a short story about a programmer.
"""
```
-### Structured Data Extraction
+Structured Data Extraction:
```plx
[pipe.extract_info]
type = "PipeLLM"
-definition = "Extract information"
+description = "Extract information"
inputs = { text = "Text" }
output = "PersonInfo"
-prompt_template = """
+prompt = """
Extract person information from this text:
@text
"""
```
-### System Prompts
-Add system-level instructions:
+Supports system instructions:
```plx
[pipe.expert_analysis]
type = "PipeLLM"
-definition = "Expert analysis"
+description = "Expert analysis"
output = "Analysis"
system_prompt = "You are a data analysis expert"
-prompt_template = "Analyze this data"
+prompt = "Analyze this data"
```
-### Multiple Outputs
-Generate multiple results:
+#### Multiple Outputs
+
+Generate multiple outputs (fixed number):
```plx
[pipe.generate_ideas]
type = "PipeLLM"
-definition = "Generate ideas"
+description = "Generate ideas"
output = "Idea"
nb_output = 3 # Generate exactly 3 ideas
-# OR
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
multiple_output = true # Let the LLM decide how many to generate
```
-### Vision Tasks
-Process images with VLMs:
+#### Vision
+
+Process images with VLMs (image inputs must be tagged in the prompt):
```plx
[pipe.analyze_image]
type = "PipeLLM"
-definition = "Analyze image"
-inputs = { image = "Image" } # `image` is the name of the stuff that contains the Image. If its in a stuff, you can add something like `{ "page.image": "Image" }
+description = "Analyze image"
+inputs = { image = "Image" }
output = "ImageAnalysis"
-prompt_template = "Describe what you see in this image"
+prompt = """
+Describe what you see in this image:
+
+$image
+"""
```
-# PipeOCR Guide
+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."
+```
-## Purpose
+#### Writing prompts for PipeLLM
-Extract text and images from an image or a PDF
+**Insert stuff inside a tagged block**
-## Basic Usage
+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.
-### Simple Text Generation
+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 = "PipeOcr"
-definition = "extract the information"
-inputs = { ocr_input = "PDF" } # or { ocr_input = "Image" } if its an image. This is the only input
+type = "PipeExtract"
+description = "extract the information"
+inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
output = "Page"
```
-The input ALWAYS HAS TO BE `ocr_input` and the value is either of concept `Image` or `Pdf`.
+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 PipeOcr is outputing a `ListContent` of `Page`
+It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
```python
class TextAndImagesContent(StuffContent):
- text: Optional[TextContent]
- images: Optional[List[ImageContent]]
+ text: TextContent | None
+ images: list[ImageContent] | None
class PageContent(StructuredContent): # CONCEPT IS "Page"
text_and_images: TextAndImagesContent
- page_view: Optional[ImageContent] = None
+ 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.
-This rule explains how to write prompt templates in PipeLLM definitions.
+### PipeCompose operator
-## Insert stuff inside a tagged block
+The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-If the inserted text is supposedly 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.
+#### Basic Usage
-Example template:
+Simple Template Composition:
```plx
-prompt_template = """
-Match the expense with its corresponding invoice:
+[pipe.compose_report]
+type = "PipeCompose"
+description = "Compose a report using template"
+inputs = { data = "ReportData" }
+output = "Text"
+template = """
+## Report Summary
-@expense
+Based on the analysis:
+$data
-@invoices
+Generated on: {{ current_date }}
"""
```
-In this example, 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 doens't need to be explictly written in the prompt template.
-**DO NOT write things like "Here is the expense: @expense".**
-**DO write simply "@expense" alone in an isolated line.**
+Using Named Templates:
+```plx
+[pipe.use_template]
+type = "PipeCompose"
+description = "Use a predefined template"
+inputs = { content = "Text" }
+output = "Text"
+template_name = "standard_report_template"
+```
-## Insert stuff inline
+Using Nested Template Section (for more control):
+```plx
+[pipe.advanced_template]
+type = "PipeCompose"
+description = "Use advanced template settings"
+inputs = { data = "ReportData" }
+output = "Text"
-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.
+[pipe.advanced_template.template]
+template = "Report: $data"
+category = "html"
+templating_style = { tag_style = "square_brackets", text_format = "html" }
+```
-Example template:
+CRM Email Template:
```plx
-prompt_template = """
-Your goal is to summarize everything related to $topic in the provided text:
+[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
-@text
+Hi $customer.first_name,
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
+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
"""
```
-Here, $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.
+#### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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)
-**DO NOT write "$topic" alone in an isolated line.**
-**DO write things like "Write an essay about $topic" included in an actual sentence.**
+#### Input Requirements
-# Example to execute a pipeline
+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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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.hub import get_pipeline_tracker, get_report_delegate
from pipelex.pipelex import Pipelex
from pipelex.pipeline.execute import execute_pipeline
-from pipelex_libraries.pipelines.examples.extract_gantt.gantt import GanttChart
+
+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"
@@ -433,31 +892,32 @@ async def extract_gantt(image_url: str) -> GanttChart:
return pipe_output.main_stuff_as(content_type=GanttChart)
-# start Pipelex
+## start Pipelex
Pipelex.make()
-# run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(IMAGE_URL))
-
-# Display cost report (tokens used and cost)
-get_report_delegate().generate_report()
-# output results
+## run sample using asyncio
+gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
pretty_print(gantt_chart, title="Gantt Chart")
-get_pipeline_tracker().output_flowchart()
```
-The input memory is a dictionary of key-value pairs, 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:
+### 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
-ImplicitMemory = Dict[str, StuffContentOrData]
+StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
+ImplicitMemory = 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.
+## 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",
input_memory={
@@ -465,18 +925,18 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
- "ocr_input": PDFContent(url=pdf_url),
+ "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:
+## 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",
input_memory={
@@ -484,9 +944,9 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
@@ -497,7 +957,7 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# Here is a more complex example with multiple inputs assigned using different ways:
+## 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",
@@ -512,64 +972,141 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
)
```
-ALWAYS RUN `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
+### Using the outputs of a pipeline
-# Rules to choose LLM models used in PipeLLMs.
+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:
-## LLM Handles
+```python
-In order to use it in a pipe, an LLM is referenced by its llm_handle and possibly by an llm_preset.
-Both llm_handles and llm_presets are defined in this toml config file: [base_llm_deck.toml](mdc:your/path/to/pipelex/config/folder/llm_deck/base_llm_deck.toml)
+class PipeOutput(BaseModel):
+ working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
+ pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-## LLM Handles
+ @property
+ def main_stuff(self) -> Stuff:
+ ...
-An llm_handle matches the handle (an id of sorts) with the full specification of the LLM to use, i.e.:
-- llm_name
-- llm_version
-- llm_platform_choice
+ def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
+ ...
-The declaration of llm_handles looks like this in toml syntax:
-```toml
-[llm_handles]
-gpt-4o-2024-11-20 = { llm_name = "gpt-4o", llm_version = "2024-11-20" }
+ 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:
+ ...
```
-In mosty cases, we only want to use version "latest" and llm_platform_choice "default" in which case the declaration is simply a match of the llm_handle to the llm_name, like this:
+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
-best-claude = "claude-4-opus"
-best-gemini = "gemini-2.5-pro"
-best-mistral = "mistral-large"
+[aliases]
+base-claude = "claude-4.5-sonnet"
+base-gpt = "gpt-5"
+base-gemini = "gemini-2.5-flash"
+base-mistral = "mistral-medium"
```
-And of course, llm_handles are automatically assigned for all models by their name, with version "latest" and llm_platform_choice "default".
+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
+### 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"
-definition = "Write text about Hello World."
+description = "Write text about Hello World."
output = "Text"
-llm = { llm_handle = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
-prompt_template = """
+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
+### 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_reason = { llm_handle = "o4-mini", temperature = 1, max_tokens = "auto" }
-llm_to_extract_invoice = { llm_handle = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
+llm_to_reason = { 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:
@@ -577,11 +1114,11 @@ The interest is that these presets can be used to set the LLM choice in a PipeLL
```plx
[pipe.extract_invoice]
type = "PipeLLM"
-definition = "Extract invoice information from an invoice text transcript"
+description = "Extract invoice information from an invoice text transcript"
inputs = { invoice_text = "InvoiceText" }
output = "Invoice"
-llm = "llm_to_extract_invoice"
-prompt_template = """
+model = "llm_to_extract_invoice"
+prompt = """
Extract invoice information from this invoice:
The category of this invoice is: $invoice_details.category.
@@ -590,8 +1127,9 @@ The category of this invoice is: $invoice_details.category.
"""
```
-The setting here `llm = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
+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 in [overrides.toml](your/path/to/pipelex/config/folder/llm_deck/overrides.toml).
+You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
+
diff --git a/.gitignore b/.gitignore
index 7540a5d..29670a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,10 +30,4 @@ results/
# temps
temp/
pipelex_super.toml
-
-# pipelex_libraries
-
base_llm_deck.toml
-pipelex_libraries/pipelines/base_library
-base_templates.toml
-pipelex_super.toml
diff --git a/.pipelex/inference/backends.toml b/.pipelex/inference/backends.toml
index 00b7cd4..459b469 100644
--- a/.pipelex/inference/backends.toml
+++ b/.pipelex/inference/backends.toml
@@ -2,48 +2,56 @@
endpoint = "https://inference.pipelex.com/v1"
api_key = "${PIPELEX_INFERENCE_API_KEY}"
-[blackboxai]
-enabled = false
-endpoint = "https://api.blackbox.ai/v1"
-api_key = "${BLACKBOX_API_KEY}"
-
-[openai]
-enabled = false
-api_key = "${OPENAI_API_KEY}"
-
[azure_openai]
enabled = false
endpoint = "${AZURE_API_BASE}"
api_key = "${AZURE_API_KEY}"
api_version = "${AZURE_API_VERSION}"
+[bedrock]
+enabled = false
+aws_region = "${AWS_REGION}"
+
+[google]
+enabled = false
+api_key = "${GOOGLE_API_KEY}"
+
+[vertexai]
+enabled = false
+gcp_project_id = "${GCP_PROJECT_ID}"
+gcp_location = "${GCP_LOCATION}"
+gcp_credentials_file_path = "${GCP_CREDENTIALS_FILE_PATH}"
+
+[openai]
+enabled = false
+api_key = "${OPENAI_API_KEY}"
+
[anthropic]
enabled = false
api_key = "${ANTHROPIC_API_KEY}"
claude_4_tokens_limit = 8192
-[ollama]
+[mistral]
enabled = false
-endpoint = "http://localhost:11434/v1"
+api_key = "${MISTRAL_API_KEY}"
[xai]
enabled = false
endpoint = "https://api.x.ai/v1"
api_key = "${XAI_API_KEY}"
-[bedrock]
+[ollama]
enabled = false
-aws_region = "${AWS_REGION}"
+endpoint = "http://localhost:11434/v1"
-[vertexai]
+[blackboxai]
enabled = false
-gcp_project_id = "${GCP_PROJECT_ID}"
-gcp_location = "${GCP_LOCATION}"
-gcp_credentials_file_path = "${GCP_CREDENTIALS_FILE_PATH}"
+endpoint = "https://api.blackbox.ai/v1"
+api_key = "${BLACKBOX_API_KEY}"
-[mistral]
+[fal]
enabled = false
-api_key = "${MISTRAL_API_KEY}"
+api_key = "${FAL_API_KEY}"
[internal] # software-only backend, runs internally, without AI
enabled = true
diff --git a/.pipelex/inference/backends/anthropic.toml b/.pipelex/inference/backends/anthropic.toml
index 0dc8551..e0daf1c 100644
--- a/.pipelex/inference/backends/anthropic.toml
+++ b/.pipelex/inference/backends/anthropic.toml
@@ -1,6 +1,32 @@
-default_sdk = "anthropic"
-default_prompting_target = "anthropic"
+################################################################################
+# Anthropic Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Anthropic Claude models.
+# It contains model definitions for various Claude language models
+# accessible through the Anthropic API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["claude-3.5-sonnet"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "anthropic"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Claude 3 Series ----------------------------------------------------------
[claude-3-haiku]
model_id = "claude-3-haiku-20240307"
max_tokens = 4096
@@ -17,22 +43,7 @@ outputs = ["text", "structured"]
max_prompt_images = 100
costs = { input = 15.0, output = 75.0 }
-["claude-3.5-sonnet"]
-model_id = "claude-3-5-sonnet-20240620"
-max_tokens = 8192
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-max_prompt_images = 100
-costs = { input = 3.0, output = 15.0 }
-
-["claude-3.5-sonnet-v2"]
-model_id = "claude-3-5-sonnet-20241022"
-max_tokens = 8192
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-max_prompt_images = 100
-costs = { input = 3.0, output = 15.0 }
-
+# --- Claude 3.7 Series --------------------------------------------------------
["claude-3.7-sonnet"]
model_id = "claude-3-7-sonnet-20250219"
max_tokens = 8192
@@ -41,6 +52,7 @@ outputs = ["text", "structured"]
max_prompt_images = 100
costs = { input = 3.0, output = 15.0 }
+# --- Claude 4 Series ----------------------------------------------------------
[claude-4-sonnet]
model_id = "claude-sonnet-4-20250514"
max_tokens = 64000
@@ -57,6 +69,7 @@ outputs = ["text", "structured"]
max_prompt_images = 100
costs = { input = 3.0, output = 15.0 }
+# --- Claude 4.1 Series --------------------------------------------------------
["claude-4.1-opus"]
model_id = "claude-opus-4-1-20250805"
max_tokens = 32000
@@ -65,3 +78,11 @@ outputs = ["text", "structured"]
max_prompt_images = 100
costs = { input = 3.0, output = 15.0 }
+# --- Claude 4.5 Series --------------------------------------------------------
+["claude-4.5-sonnet"]
+model_id = "claude-sonnet-4-5-20250929"
+max_tokens = 64000
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 100
+costs = { input = 3.0, output = 15.0 }
diff --git a/.pipelex/inference/backends/azure_openai.toml b/.pipelex/inference/backends/azure_openai.toml
index b261788..26e01b4 100644
--- a/.pipelex/inference/backends/azure_openai.toml
+++ b/.pipelex/inference/backends/azure_openai.toml
@@ -1,6 +1,32 @@
-default_sdk = "azure_openai"
-default_prompting_target = "openai"
+################################################################################
+# Azure OpenAI Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Azure OpenAI models.
+# It contains model definitions for OpenAI models deployed on Azure
+# accessible through the Azure OpenAI API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gpt-4.1"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "azure_openai"
+prompting_target = "openai"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- GPT-4o Series ------------------------------------------------------------
[gpt-4o]
model_id = "gpt-4o-2024-11-20"
inputs = ["text", "images"]
@@ -13,6 +39,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.15, output = 0.6 }
+# --- GPT-4.1 Series -----------------------------------------------------------
["gpt-4.1"]
model_id = "gpt-4.1-2025-04-14"
inputs = ["text", "images"]
@@ -31,6 +58,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.1, output = 0.4 }
+# --- o Series ----------------------------------------------------------------
[o1-mini]
model_id = "o1-mini-2024-09-12"
inputs = ["text"]
@@ -49,6 +77,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 1.1, output = 4.4 }
+# --- GPT-5 Series -------------------------------------------------------------
[gpt-5-mini]
model_id = "gpt-5-mini-2025-08-07"
inputs = ["text", "images"]
diff --git a/.pipelex/inference/backends/bedrock.toml b/.pipelex/inference/backends/bedrock.toml
index 447c76f..e572302 100644
--- a/.pipelex/inference/backends/bedrock.toml
+++ b/.pipelex/inference/backends/bedrock.toml
@@ -1,6 +1,32 @@
-default_sdk = "bedrock_aioboto3"
-default_prompting_target = "anthropic"
+################################################################################
+# AWS Bedrock Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for AWS Bedrock models.
+# It contains model definitions for various language models
+# accessible through the AWS Bedrock service.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["claude-3.5-sonnet"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "bedrock_aioboto3"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Mistral Models -----------------------------------------------------------
[bedrock-mistral-large]
model_id = "mistral.mistral-large-2407-v1:0"
max_tokens = 8192
@@ -8,6 +34,7 @@ inputs = ["text"]
outputs = ["text"]
costs = { input = 4.0, output = 12.0 }
+# --- Meta Llama Models --------------------------------------------------------
[bedrock-meta-llama-3-3-70b-instruct]
model_id = "us.meta.llama3-3-70b-instruct-v1:0"
max_tokens = 8192
@@ -16,6 +43,7 @@ outputs = ["text"]
# TODO: find out the actual cost per million tokens for llama3 on bedrock
costs = { input = 3.0, output = 15.0 }
+# --- Amazon Nova Models -------------------------------------------------------
[bedrock-nova-pro]
model_id = "us.amazon.nova-pro-v1:0"
max_tokens = 5120
@@ -24,28 +52,7 @@ outputs = ["text"]
# TODO: find out the actual cost per million tokens for nova on bedrock
costs = { input = 3.0, output = 15.0 }
-################################################################################
-# Anthropic Models
-################################################################################
-
-["claude-3.5-sonnet"]
-sdk = "bedrock_anthropic"
-model_id = "us.anthropic.claude-3-5-sonnet-20240620-v1:0"
-max_tokens = 8192
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-max_prompt_images = 100
-costs = { input = 3.0, output = 15.0 }
-
-["claude-3.5-sonnet-v2"]
-sdk = "bedrock_anthropic"
-model_id = "anthropic.claude-3-5-sonnet-20241022-v2:0"
-max_tokens = 8192
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-max_prompt_images = 100
-costs = { input = 3.0, output = 15.0 }
-
+# --- Claude LLMs --------------------------------------------------------------
["claude-3.7-sonnet"]
sdk = "bedrock_anthropic"
model_id = "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
@@ -82,3 +89,13 @@ outputs = ["text", "structured"]
max_prompt_images = 100
costs = { input = 3.0, output = 15.0 }
+# anthropic.claude-sonnet-4-5-20250929-v1:0,Anthropic,arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0,
+
+["claude-4.5-sonnet"]
+sdk = "bedrock_anthropic"
+model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
+max_tokens = 8192
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 100
+costs = { input = 3.0, output = 15.0 }
diff --git a/.pipelex/inference/backends/blackboxai.toml b/.pipelex/inference/backends/blackboxai.toml
index 14ca782..4202fd7 100644
--- a/.pipelex/inference/backends/blackboxai.toml
+++ b/.pipelex/inference/backends/blackboxai.toml
@@ -1,8 +1,31 @@
-
-default_sdk = "openai"
-default_prompting_target = "anthropic"
-
-# OpenAI Models
+################################################################################
+# BlackBoxAI Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for BlackBoxAI models.
+# It contains model definitions for various language models from different providers
+# accessible through the BlackBoxAI API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gpt-4.5-preview"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- OpenAI Models ------------------------------------------------------------
[gpt-4o-mini]
model_id = "blackboxai/openai/gpt-4o-mini"
inputs = ["text", "images"]
@@ -27,13 +50,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 1.10, output = 4.40 }
-["gpt-4.5-preview"]
-model_id = "blackboxai/openai/gpt-4.5-preview"
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-costs = { input = 75.00, output = 150.00 }
-
-# Anthropic Models
+# --- Claude LLMs --------------------------------------------------------------
["claude-3.5-haiku"]
model_id = "blackboxai/anthropic/claude-3.5-haiku"
inputs = ["text", "images"]
@@ -64,7 +81,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 3.00, output = 15.00 }
-# Google Models
+# --- Google Models ------------------------------------------------------------
["gemini-2.5-flash"]
model_id = "blackboxai/google/gemini-2.5-flash"
inputs = ["text", "images"]
@@ -83,26 +100,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.04, output = 0.15 }
-# Free Models
-[deepseek-chat-free]
-model_id = "blackboxai/deepseek/deepseek-chat:free"
-inputs = ["text"]
-outputs = ["text", "structured"]
-costs = { input = 0.00, output = 0.00 }
-
-[deepseek-r1-free]
-model_id = "blackboxai/deepseek/deepseek-r1:free"
-inputs = ["text"]
-outputs = ["text", "structured"]
-costs = { input = 0.00, output = 0.00 }
-
-["llama-3.3-70b-instruct-free"]
-model_id = "blackboxai/meta-llama/llama-3.3-70b-instruct:free"
-inputs = ["text"]
-outputs = ["text", "structured"]
-costs = { input = 0.00, output = 0.00 }
-
-# Mistral Models
+# --- Mistral Models -----------------------------------------------------------
[mistral-large]
model_id = "blackboxai/mistralai/mistral-large"
inputs = ["text"]
@@ -115,33 +113,33 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 2.00, output = 6.00 }
-# Cost-Effective Models
+# --- Meta Llama Models --------------------------------------------------------
["llama-3.3-70b-instruct"]
model_id = "blackboxai/meta-llama/llama-3.3-70b-instruct"
inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 0.04, output = 0.12 }
-["qwen-2.5-72b-instruct"]
-model_id = "blackboxai/qwen/qwen-2.5-72b-instruct"
-inputs = ["text"]
-outputs = ["text", "structured"]
-costs = { input = 0.12, output = 0.39 }
-
-# Vision Models
["llama-3.2-11b-vision-instruct"]
model_id = "blackboxai/meta-llama/llama-3.2-11b-vision-instruct"
inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.05, output = 0.05 }
+# --- Qwen Models --------------------------------------------------------------
+["qwen-2.5-72b-instruct"]
+model_id = "blackboxai/qwen/qwen-2.5-72b-instruct"
+inputs = ["text"]
+outputs = ["text", "structured"]
+costs = { input = 0.12, output = 0.39 }
+
["qwen2.5-vl-72b-instruct"]
model_id = "blackboxai/qwen/qwen2.5-vl-72b-instruct"
inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.25, output = 0.75 }
-# Amazon Nova Models
+# --- Amazon Nova Models -------------------------------------------------------
[nova-micro-v1]
model_id = "blackboxai/amazon/nova-micro-v1"
inputs = ["text"]
@@ -154,3 +152,27 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 0.06, output = 0.24 }
+################################################################################
+# FREE MODELS
+################################################################################
+
+# --- DeepSeek Free Models -----------------------------------------------------
+[deepseek-chat-free]
+model_id = "blackboxai/deepseek/deepseek-chat:free"
+inputs = ["text"]
+outputs = ["text", "structured"]
+costs = { input = 0.00, output = 0.00 }
+
+[deepseek-r1-free]
+model_id = "blackboxai/deepseek/deepseek-r1:free"
+inputs = ["text"]
+outputs = ["text", "structured"]
+costs = { input = 0.00, output = 0.00 }
+
+# --- Meta Llama Free Models ---------------------------------------------------
+["llama-3.3-70b-instruct-free"]
+model_id = "blackboxai/meta-llama/llama-3.3-70b-instruct:free"
+inputs = ["text"]
+outputs = ["text", "structured"]
+costs = { input = 0.00, output = 0.00 }
+
diff --git a/.pipelex/inference/backends/fal.toml b/.pipelex/inference/backends/fal.toml
new file mode 100644
index 0000000..ed15e6b
--- /dev/null
+++ b/.pipelex/inference/backends/fal.toml
@@ -0,0 +1,54 @@
+################################################################################
+# FAL Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for FAL (Fast AI Labs) models.
+# It contains model definitions for various image generation models
+# accessible through the FAL API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["flux-pro/v1.1"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "img_gen"
+sdk = "fal"
+prompting_target = "fal"
+
+################################################################################
+# IMAGE GENERATION MODELS
+################################################################################
+
+# --- Flux Pro Series ----------------------------------------------------------
+[flux-pro]
+model_id = "fal-ai/flux-pro"
+inputs = ["text"]
+outputs = ["image"]
+costs = { input = 0.05, output = 0.0 }
+
+["flux-pro/v1.1"]
+model_id = "fal-ai/flux-pro/v1.1"
+inputs = ["text"]
+outputs = ["image"]
+costs = { input = 0.05, output = 0.0 }
+
+["flux-pro/v1.1-ultra"]
+model_id = "fal-ai/flux-pro/v1.1-ultra"
+inputs = ["text"]
+outputs = ["image"]
+costs = { input = 0.06, output = 0.0 }
+
+# --- SDXL models --------------------------------------------------------------
+[fast-lightning-sdxl]
+model_id = "fal-ai/fast-lightning-sdxl"
+inputs = ["text"]
+outputs = ["image"]
+costs = { input = 0.0003, output = 0.0 }
+
diff --git a/.pipelex/inference/backends/google.toml b/.pipelex/inference/backends/google.toml
new file mode 100644
index 0000000..7fdc6cd
--- /dev/null
+++ b/.pipelex/inference/backends/google.toml
@@ -0,0 +1,57 @@
+################################################################################
+# Google Gemini API Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Google Gemini API models.
+# It contains model definitions for Gemini language models
+# accessible through the Google Gemini API (not VertexAI).
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gemini-2.0-flash"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "google"
+prompting_target = "gemini"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Gemini 2.0 Series ----------------------------------------
+["gemini-2.0-flash"]
+model_id = "gemini-2.0-flash"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 3000
+costs = { input = 0.10, output = 0.40 }
+
+# --- Gemini 2.5 Series ----------------------------------------
+["gemini-2.5-pro"]
+model_id = "gemini-2.5-pro"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 3000
+costs = { input = 1.25, output = 10.0 }
+
+["gemini-2.5-flash"]
+model_id = "gemini-2.5-flash"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 3000
+costs = { input = 0.30, output = 2.50 }
+
+["gemini-2.5-flash-lite"]
+model_id = "gemini-2.5-flash-lite"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+max_prompt_images = 3000
+costs = { input = 0.10, output = 0.40 }
diff --git a/.pipelex/inference/backends/internal.toml b/.pipelex/inference/backends/internal.toml
index da42bf7..0dbd6e7 100644
--- a/.pipelex/inference/backends/internal.toml
+++ b/.pipelex/inference/backends/internal.toml
@@ -1,4 +1,21 @@
+################################################################################
+# Internal Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for internal software-only models.
+# These models run internally without external APIs or AI services.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# TEXT EXTRACTION MODELS
+################################################################################
+
+# --- PyPDFium2 Text Extractor -------------------------------------------------
[pypdfium2-extract-text]
model_type = "text_extractor"
sdk = "pypdfium2"
diff --git a/.pipelex/inference/backends/mistral.toml b/.pipelex/inference/backends/mistral.toml
index b452cef..634736c 100644
--- a/.pipelex/inference/backends/mistral.toml
+++ b/.pipelex/inference/backends/mistral.toml
@@ -1,6 +1,32 @@
-default_sdk = "mistral"
-default_prompting_target = "mistral"
+################################################################################
+# Mistral Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Mistral AI models.
+# It contains model definitions for various Mistral language models and specialized models
+# accessible through the Mistral API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["ministral-3b"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "mistral"
+prompting_target = "mistral"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Ministral Series ---------------------------------------------------------
[ministral-3b]
model_id = "ministral-3b-latest"
max_tokens = 131072
@@ -15,6 +41,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 0.1, output = 0.1 }
+# --- Mistral 7B Series --------------------------------------------------------
[mistral-7b-2312]
model_id = "mistral-large-2402"
max_tokens = 32768
@@ -22,6 +49,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 0.25, output = 0.25 }
+# --- Mistral 8x7B Series ------------------------------------------------------
[mistral-8x7b-2312]
model_id = "open-mixtral-8x7b"
max_tokens = 32768
@@ -29,6 +57,7 @@ inputs = ["text"]
outputs = ["text"]
costs = { input = 0.7, output = 0.7 }
+# --- Mistral Codestral Series -------------------------------------------------
[mistral-codestral-2405]
model_id = "codestral-2405"
max_tokens = 262144
@@ -36,6 +65,7 @@ inputs = ["text"]
outputs = ["text"]
costs = { input = 1.0, output = 3.0 }
+# --- Mistral Large Series -----------------------------------------------------
[mistral-large-2402]
model_id = "mistral-large-2402"
max_tokens = 32768
@@ -50,6 +80,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 4.0, output = 12.0 }
+# --- Mistral Small Series -----------------------------------------------------
[mistral-small-2402]
model_id = "mistral-small-2402"
max_tokens = 32768
@@ -64,6 +95,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 1.0, output = 3.0 }
+# --- Pixtral Series -----------------------------------------------------------
[pixtral-12b]
model_id = "pixtral-12b-latest"
max_tokens = 131072
@@ -78,6 +110,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 2.0, output = 6.0 }
+# --- Mistral Medium Series ----------------------------------------------------
[mistral-medium]
model_id = "mistral-medium-latest"
max_tokens = 128000
@@ -92,6 +125,11 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.4, output = 2.0 }
+################################################################################
+# OCR MODELS
+################################################################################
+
+# --- OCR Models ---------------------------------------------------------------
[mistral-ocr]
model_type = "text_extractor"
model_id = "mistral-ocr-latest"
diff --git a/.pipelex/inference/backends/ollama.toml b/.pipelex/inference/backends/ollama.toml
index 2b66dee..7a56b73 100644
--- a/.pipelex/inference/backends/ollama.toml
+++ b/.pipelex/inference/backends/ollama.toml
@@ -1,6 +1,32 @@
-default_sdk = "openai"
-default_prompting_target = "anthropic"
+################################################################################
+# Ollama Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Ollama models.
+# It contains model definitions for local language models
+# accessible through the Ollama API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["mistral-small3.1-24b"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Gemma Models -------------------------------------------------------------
[gemma3-4b]
model_id = "gemma3:4b"
inputs = ["text"]
@@ -8,6 +34,7 @@ outputs = ["text"]
max_prompt_images = 3000
costs = { input = 0, output = 0 }
+# --- Llama Models -------------------------------------------------------------
[llama4-scout]
model_id = "llama4:scout"
inputs = ["text"]
@@ -15,6 +42,7 @@ outputs = ["text"]
max_prompt_images = 3000
costs = { input = 0, output = 0 }
+# --- Mistral Models -----------------------------------------------------------
["mistral-small3.1-24b"]
model_id = "mistral-small3.1:24b"
inputs = ["text"]
@@ -22,6 +50,7 @@ outputs = ["text"]
max_prompt_images = 3000
costs = { input = 0, output = 0 }
+# --- Qwen Models --------------------------------------------------------------
[qwen3-8b]
model_id = "qwen3:8b"
inputs = ["text"]
diff --git a/.pipelex/inference/backends/openai.toml b/.pipelex/inference/backends/openai.toml
index 6e36194..4eabe74 100644
--- a/.pipelex/inference/backends/openai.toml
+++ b/.pipelex/inference/backends/openai.toml
@@ -1,12 +1,39 @@
-default_sdk = "openai"
-default_prompting_target = "openai"
-
+################################################################################
+# OpenAI Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for OpenAI models.
+# It contains model definitions for various LLM and image generation models
+# accessible through the OpenAI API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gpt-4.1"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "openai"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- GPT-3.5 Series -----------------------------------------------------------
["gpt-3.5-turbo"]
model_id = "gpt-3.5-turbo-1106"
inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 0.5, output = 1.5 }
+# --- GPT-4 Series -------------------------------------------------------------
[gpt-4]
model_id = "gpt-4"
costs = { input = 30.0, output = 60.0 }
@@ -17,6 +44,7 @@ inputs = ["text"]
outputs = ["text", "structured"]
costs = { input = 10.0, output = 30.0 }
+# --- GPT-4o Series ------------------------------------------------------------
[gpt-4o-2024-11-20]
model_id = "gpt-4o-2024-11-20"
inputs = ["text", "images"]
@@ -41,6 +69,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.15, output = 0.6 }
+# --- GPT-4.1 Series -----------------------------------------------------------
["gpt-4.1"]
model_id = "gpt-4.1"
inputs = ["text", "images"]
@@ -59,6 +88,7 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.1, output = 0.4 }
+# --- o Series ----------------------------------------------------------------
[o1-mini]
model_id = "o1-mini"
inputs = ["text"]
@@ -94,6 +124,7 @@ outputs = ["text", "structured"]
costs = { input = 1.1, output = 4.4 }
constraints = ["temperature_must_be_1"]
+# --- GPT-5 Series -------------------------------------------------------------
[gpt-5]
model_id = "gpt-5"
inputs = ["text", "images"]
@@ -122,3 +153,15 @@ outputs = ["text"]
costs = { input = 1.25, output = 10.0 }
constraints = ["temperature_must_be_1"]
+################################################################################
+# IMAGE GENERATION MODELS
+################################################################################
+
+# --- OpenAI Image Generation --------------------------------------------------
+[gpt-image-1]
+model_type = "img_gen"
+model_id = "gpt-image-1"
+inputs = ["text"]
+outputs = ["image"]
+costs = { input = 0.04, output = 0.0 }
+
diff --git a/.pipelex/inference/backends/perplexity.toml b/.pipelex/inference/backends/perplexity.toml
index 4eb004d..ea90551 100644
--- a/.pipelex/inference/backends/perplexity.toml
+++ b/.pipelex/inference/backends/perplexity.toml
@@ -1,6 +1,32 @@
-default_sdk = "openai"
-default_prompting_target = "anthropic"
+################################################################################
+# Perplexity Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Perplexity AI models.
+# It contains model definitions for Sonar and reasoning models
+# accessible through the Perplexity API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["sonar-pro"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Sonar Series -------------------------------------------------------------
[sonar-pro]
model_id = "sonar-pro"
inputs = ["text"]
@@ -16,6 +42,7 @@ model_id = "sonar-deep-research"
inputs = ["text"]
outputs = ["text"]
+# --- Sonar Reasoning Series ---------------------------------------------------
[sonar-reasoning-pro]
model_id = "sonar-reasoning-pro"
inputs = ["text"]
@@ -26,7 +53,8 @@ model_id = "sonar-reasoning"
inputs = ["text"]
outputs = ["text"]
+# --- DeepSeek Models -----------------------------------------------------------
[perplexity-deepseek-r1]
model_id = "r1-1776"
inputs = ["text"]
-outputs = ["text"]
+outputs = ["text"]
\ No newline at end of file
diff --git a/.pipelex/inference/backends/pipelex_inference.toml b/.pipelex/inference/backends/pipelex_inference.toml
index a9fdcf2..d850714 100644
--- a/.pipelex/inference/backends/pipelex_inference.toml
+++ b/.pipelex/inference/backends/pipelex_inference.toml
@@ -1,19 +1,44 @@
-default_sdk = "openai"
-default_prompting_target = "anthropic"
+################################################################################
+# Pipelex Inference Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for the Pipelex Inference backend.
+# It contains model definitions for various LLM and image generation models
+# accessible through the Pipelex unified inference API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gpt-4.1"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- OpenAI LLMs --------------------------------------------------------------
+[gpt-4o]
+model_id = "pipelex/gpt-4o"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+costs = { input = 2.75, output = 11.00 }
-# OpenAI Models
[gpt-4o-mini]
model_id = "pipelex/gpt-4o-mini"
inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.17, output = 0.66 }
-[gpt-4o]
-model_id = "pipelex/gpt-4o"
-inputs = ["text", "images"]
-outputs = ["text", "structured"]
-costs = { input = 2.75, output = 11 }
-
["gpt-4.1"]
model_id = "pipelex/gpt-4.1"
inputs = ["text", "images"]
@@ -50,13 +75,7 @@ inputs = ["text", "images"]
outputs = ["text"]
costs = { input = 1.25, output = 10.00 }
-[gpt-image-1]
-model_id = "pipelex/gpt-image-1"
-inputs = ["text"]
-outputs = ["images"]
-costs = { input = 5, output = 0.01 }
-
-# Anthropic Models
+# --- Claude LLMs --------------------------------------------------------------
["claude-4-sonnet"]
model_id = "pipelex/claude-4-sonnet"
inputs = ["text", "images"]
@@ -69,7 +88,13 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 15, output = 75 }
-# Google Models using Google API
+["claude-4.5-sonnet"]
+model_id = "pipelex/claude-4.5-sonnet"
+inputs = ["text", "images"]
+outputs = ["text", "structured"]
+costs = { input = 3, output = 15 }
+
+# --- Gemini LLMs --------------------------------------------------------------
["gemini-2.0-flash"]
model_id = "gemini/gemini-2.0-flash"
inputs = ["text", "images"]
@@ -88,7 +113,8 @@ inputs = ["text", "images"]
outputs = ["text", "structured"]
costs = { input = 0.10, output = 0.40 }
-# XAI Models
+# --- XAI LLMs --------------------------------------------------------------
+
[grok-3]
model_id = "grok-3"
inputs = ["text"]
@@ -101,9 +127,9 @@ inputs = ["text"]
outputs = ["text"]
costs = { input = 0.3, output = 0.5 }
-# Flux Models
-[fast-lightning-sdxl]
-model_id = "pipelex/fast-lightning-sdxl"
-inputs = ["text"]
-outputs = ["images"]
-costs = { input = 0.01, output = 0.01 }
+################################################################################
+# OCR and IMAGE GENERATION MODELS
+################################################################################
+
+# We are still working in giving you acces to OCR and image generation models
+# and to the best models from Mistral through the Pipelex Inference backend.
diff --git a/.pipelex/inference/backends/vertexai.toml b/.pipelex/inference/backends/vertexai.toml
index e476cfc..cb8edb1 100644
--- a/.pipelex/inference/backends/vertexai.toml
+++ b/.pipelex/inference/backends/vertexai.toml
@@ -1,6 +1,32 @@
-default_sdk = "openai"
-default_prompting_target = "gemini"
+################################################################################
+# VertexAI Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for Google VertexAI models.
+# It contains model definitions for Gemini language models
+# accessible through the Google VertexAI API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["gemini-2.0-flash"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "gemini"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Gemini 2.0 Series --------------------------------------------------------
["gemini-2.0-flash"]
model_id = "google/gemini-2.0-flash"
inputs = ["text", "images"]
@@ -8,6 +34,7 @@ outputs = ["text", "structured"]
max_prompt_images = 3000
costs = { input = 0.1, output = 0.4 }
+# --- Gemini 2.5 Series --------------------------------------------------------
["gemini-2.5-pro"]
model_id = "google/gemini-2.5-pro"
inputs = ["text", "images"]
@@ -20,5 +47,4 @@ model_id = "google/gemini-2.5-flash"
inputs = ["text", "images"]
outputs = ["text", "structured"]
max_prompt_images = 3000
-costs = { input = 0.30, output = 2.50 }
-
+costs = { input = 0.30, output = 2.50 }
\ No newline at end of file
diff --git a/.pipelex/inference/backends/xai.toml b/.pipelex/inference/backends/xai.toml
index ea8fb58..545b905 100644
--- a/.pipelex/inference/backends/xai.toml
+++ b/.pipelex/inference/backends/xai.toml
@@ -1,6 +1,32 @@
-default_sdk = "openai"
-default_prompting_target = "anthropic"
+################################################################################
+# XAI Backend Configuration
+################################################################################
+#
+# This file defines the model specifications for XAI (formerly Twitter AI) models.
+# It contains model definitions for Grok language models
+# accessible through the XAI API.
+#
+# Configuration structure:
+# - Each model is defined in its own section with the model name as the header
+# - Headers with dots must be quoted (e.g., ["grok-3"])
+# - Model costs are in USD per million tokens (input/output)
+#
+################################################################################
+################################################################################
+# MODEL DEFAULTS
+################################################################################
+
+[defaults]
+model_type = "llm"
+sdk = "openai"
+prompting_target = "anthropic"
+
+################################################################################
+# LANGUAGE MODELS
+################################################################################
+
+# --- Grok 3 Series ------------------------------------------------------------
[grok-3]
model_id = "grok-3"
inputs = ["text"]
diff --git a/.pipelex/inference/deck/base_deck.toml b/.pipelex/inference/deck/base_deck.toml
index 51d646c..1b6fbcf 100644
--- a/.pipelex/inference/deck/base_deck.toml
+++ b/.pipelex/inference/deck/base_deck.toml
@@ -6,7 +6,7 @@
####################################################################################################
[aliases]
-base-claude = "claude-4-sonnet"
+base-claude = "claude-4.5-sonnet"
base-gpt = "gpt-5"
base-gemini = "gemini-2.5-flash"
base-mistral = "mistral-medium"
@@ -17,76 +17,122 @@ best-gemini = "gemini-2.5-pro"
best-mistral = "mistral-medium"
best-grok = "grok-3"
+cheap-gpt = "gpt-4o-mini"
cheap_llm_for_text = "gpt-4o-mini"
cheap_llm_for_object = "gpt-4o-mini"
+cheap_llm_for_vision = "gemini-2.5-flash-lite"
base_llm_for_text = "gpt-4o-mini"
base_llm_for_object = "gpt-4o-mini"
-llm_to_engineer = ["claude-4.1-opus", "gemini-2.5-pro"]
-llm_for_large_codebase = ["gemini-2.5-pro", "claude-4-sonnet", "gpt-5"]
+smart_llm = [
+ "claude-4.5-sonnet",
+ "claude-4.1-opus",
+ "claude-4-sonnet",
+ "gpt-5",
+ "gemini-2.5-pro",
+]
+llm_for_large_codebase = ["gemini-2.5-pro", "claude-4.5-sonnet", "gpt-5"]
+
+# Image generation aliases
+base-img-gen = "flux-pro/v1.1"
+best-img-gen = "flux-pro/v1.1-ultra"
+fast-img-gen = "fast-lightning-sdxl"
####################################################################################################
# LLM Presets
####################################################################################################
-[llm_presets]
+[llm.presets]
####################################################################################################
# LLM Presets — General purpose
-cheap_llm_for_text = { llm_handle = "cheap_llm_for_text", temperature = 0.5 }
-cheap_llm_for_short_text = { llm_handle = "cheap_llm_for_text", temperature = 0.5, max_tokens = 50 }
-cheap_llm_for_object = { llm_handle = "cheap_llm_for_object", temperature = 0.5 }
-cheap_llm_to_structure = { llm_handle = "cheap_llm_for_object", temperature = 0.1 }
+cheap_llm_for_text = { model = "cheap_llm_for_text", temperature = 0.5 }
+cheap_llm_for_short_text = { model = "cheap_llm_for_text", temperature = 0.5, max_tokens = 50 }
+cheap_llm_for_object = { model = "cheap_llm_for_object", temperature = 0.5 }
+cheap_llm_to_structure = { model = "cheap_llm_for_object", temperature = 0.1 }
-llm_for_testing_gen_text = { llm_handle = "base_llm_for_text", temperature = 0.5 }
-llm_for_testing_gen_object = { llm_handle = "base_llm_for_object", temperature = 0.5 }
+llm_for_testing_gen_text = { model = "base_llm_for_text", temperature = 0.5 }
+llm_for_testing_gen_object = { model = "base_llm_for_object", temperature = 0.5 }
+llm_for_testing_vision = { model = "cheap_llm_for_vision", temperature = 0.5 }
+llm_for_testing_vision_structured = { model = "cheap-gpt", temperature = 0.5 }
####################################################################################################
# LLM Presets — Specific skills
# Cheap llm
-llm_cheap = { llm_handle = "base-gemini", temperature = 0.1 }
+llm_cheap = { model = "base-gemini", temperature = 0.1 }
# Generation skills
-llm_for_factual_writing = { llm_handle = "base-gpt", temperature = 0.1 }
-llm_for_creative_writing = { llm_handle = "claude-4-sonnet", temperature = 0.9 }
+llm_for_factual_writing = { model = "base-gpt", temperature = 0.1 }
+llm_for_creative_writing = { model = "claude-4.5-sonnet", temperature = 0.9 }
# Reasoning skills
-llm_to_reason_short = { llm_handle = "base-claude", temperature = 0.5, max_tokens = 500 }
-llm_to_reason = { llm_handle = "base-claude", temperature = 1 }
-llm_to_reason_on_diagram = { llm_handle = "base-claude", temperature = 0.5 }
+llm_to_reason_short = { model = "base-claude", temperature = 0.5, max_tokens = 500 }
+llm_to_reason = { model = "base-claude", temperature = 1 }
+llm_to_reason_on_diagram = { model = "base-claude", temperature = 0.5 }
# Search and answer skills
-llm_to_answer = { llm_handle = "base-claude", temperature = 0.1 }
-llm_to_retrieve = { llm_handle = "base-gemini", temperature = 0.1 }
-llm_for_enrichment = { llm_handle = "gpt-4o", temperature = 0.1 }
-llm_to_enrich = { llm_handle = "base-claude", temperature = 0.1 }
-llm_for_question_and_excerpt_reformulation = { llm_handle = "gpt-4o", temperature = 0.9 }
+llm_to_answer = { model = "base-claude", temperature = 0.1 }
+llm_to_retrieve = { model = "base-gemini", temperature = 0.1 }
+llm_for_enrichment = { model = "gpt-4o", temperature = 0.1 }
+llm_to_enrich = { model = "base-claude", temperature = 0.1 }
+llm_for_question_and_excerpt_reformulation = { model = "gpt-4o", temperature = 0.9 }
# Engineering skills
-llm_to_engineer = { llm_handle = "base-claude", temperature = 0.2 }
-llm_to_pipelex = { llm_handle = "base-claude", temperature = 0.2 }
+llm_to_engineer = { model = "smart_llm", temperature = 0.2 }
+llm_to_write = { model = "base-gemini", temperature = 0.2 }
+llm_to_pipelex = { model = "base-claude", temperature = 0.2 }
# Image skills
-llm_to_write_imgg_prompt = { llm_handle = "base-claude", temperature = 0.2 }
-llm_to_describe_img = { llm_handle = "base-claude", temperature = 0.5 }
-llm_to_design_fashion = { llm_handle = "base-claude", temperature = 0.7 }
-llm_for_img_to_text = { llm_handle = "base-claude", temperature = 0.1 }
+llm_to_write_img_gen_prompt = { model = "base-claude", temperature = 0.2 }
+llm_to_describe_img = { model = "base-claude", temperature = 0.5 }
+llm_to_design_fashion = { model = "base-claude", temperature = 0.7 }
+llm_for_img_to_text = { model = "base-claude", temperature = 0.1 }
# Extraction skills
-llm_to_extract_diagram = { llm_handle = "base-claude", temperature = 0.5 }
-llm_to_extract_invoice = { llm_handle = "claude-4-sonnet", temperature = 0.1 }
-llm_to_extract_invoice_from_scan = { llm_handle = "base-claude", temperature = 0.5 }
-llm_to_extract_legal_terms = { llm_handle = "base-claude", temperature = 0.1 }
-llm_to_extract_tables = { llm_handle = "base-claude", temperature = 0.1 }
+llm_to_extract_diagram = { model = "base-claude", temperature = 0.5 }
+llm_to_extract_invoice = { model = "claude-4.5-sonnet", temperature = 0.1 }
+llm_to_extract_invoice_from_scan = { model = "base-claude", temperature = 0.5 }
+llm_to_extract_legal_terms = { model = "base-claude", temperature = 0.1 }
+llm_to_extract_tables = { model = "base-claude", temperature = 0.1 }
####################################################################################################
# LLM Choices
####################################################################################################
-[llm_choice_defaults]
+[llm.choice_defaults]
for_text = "cheap_llm_for_text"
for_object = "cheap_llm_for_object"
+####################################################################################################
+# OCR Presets
+####################################################################################################
+
+[extract]
+choice_default = "base_ocr_mistral"
+
+[extract.presets]
+base_ocr_mistral = { model = "mistral-ocr", max_nb_images = 100, image_min_size = 50 }
+base_extract_pypdfium2 = { model = "pypdfium2-extract-text", max_nb_images = 100, image_min_size = 50 }
+
+####################################################################################################
+# Image Generation Presets
+####################################################################################################
+
+[img_gen]
+choice_default = "base_img_gen"
+
+[img_gen.presets]
+# General purpose
+base_img_gen = { model = "base-img-gen", quality = "medium", guidance_scale = 7.5, is_moderated = true, safety_tolerance = 3 }
+fast_img_gen = { model = "fast-img-gen", nb_steps = 4, guidance_scale = 5.0, is_moderated = true, safety_tolerance = 3 }
+high_quality_img_gen = { model = "best-img-gen", quality = "high", guidance_scale = 8.0, is_moderated = true, safety_tolerance = 3 }
+
+# Specific skills
+img_gen_for_art = { model = "best-img-gen", quality = "high", guidance_scale = 9.0, is_moderated = false, safety_tolerance = 5 }
+img_gen_for_diagram = { model = "base-img-gen", quality = "medium", guidance_scale = 7.0, is_moderated = true, safety_tolerance = 2 }
+img_gen_for_mockup = { model = "base-img-gen", quality = "medium", guidance_scale = 6.5, is_moderated = true, safety_tolerance = 3 }
+img_gen_for_product = { model = "best-img-gen", quality = "high", guidance_scale = 8.5, is_moderated = true, safety_tolerance = 2 }
+img_gen_for_testing = { model = "fast-img-gen", nb_steps = 4, guidance_scale = 4.0, is_moderated = true, safety_tolerance = 4 }
diff --git a/.pipelex/inference/deck/overrides.toml b/.pipelex/inference/deck/overrides.toml
index 999cc4d..a6e8a49 100644
--- a/.pipelex/inference/deck/overrides.toml
+++ b/.pipelex/inference/deck/overrides.toml
@@ -4,7 +4,6 @@
# LLM Deck overrides
####################################################################################################
-[llm_choice_overrides]
+[llm.choice_overrides]
for_text = "disabled"
for_object = "disabled"
-
diff --git a/.pipelex/inference/routing_profiles.toml b/.pipelex/inference/routing_profiles.toml
index be9b51d..40138b7 100644
--- a/.pipelex/inference/routing_profiles.toml
+++ b/.pipelex/inference/routing_profiles.toml
@@ -6,12 +6,12 @@
# ================================================
# Which profile to use (change this to switch routing)
-active = "all_pipelex"
+active = "pipelex_first"
-# We recommend using the "all_pipelex" profile to get a head start with all models.
+# We recommend using the "pipelex_first" profile to get a head start with all models.
# The Pipelex Inference backend is currently not recommended for production use,
# but it's great for development and testing.
-# To use the Pipelex Inference backend (all_pipelex profile):
+# To use the Pipelex Inference backend (pipelex_first profile):
# 1. Join our Discord community to get your free API key (no credit card required):
# Visit https://go.pipelex.com/discord and request your key in the appropriate channel
# 2. Set the environment variable: export PIPELEX_INFERENCE_API_KEY="your-api-key"
@@ -22,32 +22,53 @@ active = "all_pipelex"
# Routing Profiles
# ============================================
-[profiles.all_pipelex]
-description = "Use Pipelex Inference backend for all models"
+[profiles.pipelex_first]
+description = "Use Pipelex Inference backend for all its supported models"
default = "pipelex_inference"
+[profiles.pipelex_first.routes]
+# Pattern matching: "model-pattern" = "backend-name"
+"gpt-*" = "pipelex_inference"
+"claude-*" = "pipelex_inference"
+"grok-*" = "pipelex_inference"
+"gemini-*" = "pipelex_inference"
+"*-sdxl" = "fal"
+"flux-*" = "fal"
+"gpt-image-1" = "openai"
+"mistral-ocr" = "mistral"
+
+[profiles.all_bedrock]
+description = "Use Bedrock backend for all its supported models"
+default = "bedrock"
+
[profiles.all_anthropic]
description = "Use Anthropic backend for all its supported models"
default = "anthropic"
+[profiles.all_gemini]
+description = "Use Google GenAI backend for all its supported models"
+default = "google"
+
[profiles.all_azure_openai]
description = "Use Azure OpenAI backend for all its supported models"
default = "azure_openai"
-[profiles.to_test_various_backends]
-description = "Route models to various backends"
-
[profiles.custom_routing]
description = "Custom routing"
default = "pipelex_inference"
[profiles.custom_routing.routes]
# Pattern matching: "model-pattern" = "backend-name"
-"gpt-*" = "pipelex_inference"
-"claude-*" = "pipelex_inference"
-"gemini-*" = "pipelex_inference"
-"grok-*" = "pipelex_inference"
-"*-sdxl" = "pipelex_inference"
+"gpt-*" = "azure_openai"
+"claude-*" = "bedrock"
+"gemini-*" = "google"
+"grok-*" = "xai"
+"*-sdxl" = "fal"
+"flux-*" = "fal"
+"gpt-image-1" = "openai"
+
+[profiles.to_test_various_backends]
+description = "Route models to various backends"
[profiles.to_test_various_backends.routes]
"gpt-5-nano" = "pipelex_inference"
@@ -56,8 +77,6 @@ default = "pipelex_inference"
"gpt-5-chat" = "azure_openai"
"claude-4-sonnet" = "pipelex_inference"
-"claude-3.5-sonnet" = "bedrock"
-"claude-3.5-sonnet-v2" = "anthropic"
"claude-3.7-sonnet" = "blackboxai"
"gemini-2.5-flash-lite" = "pipelex_inference"
@@ -82,4 +101,3 @@ default = "pipelex_inference"
# - Exact names: "gpt-4o-mini"
# - Wildcards: "claude-*" (matches all models starting with claude-)
# - Partial wildcards: "*-sonnet" (matches all sonnet variants)
-
diff --git a/.pipelex/pipelex.toml b/.pipelex/pipelex.toml
index b8f8b12..dddcbf0 100644
--- a/.pipelex/pipelex.toml
+++ b/.pipelex/pipelex.toml
@@ -1,3 +1,7 @@
+[pipelex]
+[pipelex.observer_config]
+observer_dir = "results/observer"
+
[pipelex.aws_config]
api_key_method = "env"
# The possible values are "env" and "secret_provider".
@@ -10,7 +14,7 @@ api_key_method = "env"
# OCR config
####################################################################################################
-[cogt.ocr_config]
+[cogt.extract_config]
page_output_text_file_name = "page_text.md"
[pipelex.feature_config]
diff --git a/.windsurfrules.md b/.windsurfrules.md
new file mode 100644
index 0000000..4d612f7
--- /dev/null
+++ b/.windsurfrules.md
@@ -0,0 +1,1135 @@
+
+## 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 the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- 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)
+
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. 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:
+`StaticValidationError: 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:
+
+### 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"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, 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):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+nb_output = 3 # Generate exactly 3 ideas
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+multiple_output = true # Let the LLM decide how many to generate
+```
+
+#### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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",
+ input_memory={
+ "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
+ImplicitMemory = 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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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_reason = { 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`.
+
diff --git a/AGENTS.md b/AGENTS.md
index 93859f7..4d612f7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,73 +1,75 @@
-# Pipeline Guide
+
+## Guide to write or edit pipelines using the Pipelex language in .plx files
-- Always first write your "plan" in natural langage, then transcribe it in pipelex.
-- You should ALWAYS RUN the terminal command `make validate` when you are writing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
-- Please use POSIX standard for files. (enmpty lines, no trailing whitespaces, etc.)
+- Always first write your "plan" in natural language, then transcribe it in pipelex.
+- You should ALWAYS RUN the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-# Pipeline Structure Guide
-
-## Pipeline File Naming
+### 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 structures
+- Files must be `.py` for code defining the data structures
- Use descriptive names in `snake_case`
-## Pipeline File Structure
+### Pipeline File Outline
A pipeline file has three main sections:
1. Domain statement
2. Concept definitions
3. Pipe definitions
-### Domain Statement
+#### Domain Statement
```plx
domain = "domain_name"
-definition = "Description of the domain" # Optional
+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
+#### 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" # Should be the same name as the Structure ClassName you want to output
+ConceptName = "Description of the concept"
```
-Important Rules:
+**Naming Rules:**
- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story")
-- Avoid adjectives (no "LargeText", use "Text")
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number)
-yes
-### Pipe Definitions
+- 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)
-## Pipe Base Structure
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. Use these directly or refine them when appropriate.
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-definition = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
+**Refining Native Concepts:**
+To create a concept that specializes a native concept without adding fields:
-DO NOT WRITE:
```plx
-[pipe.your_pipe_name]
-type = "pipe_sequence"
+[concept.Landscape]
+description = "A scenic outdoor photograph"
+refines = "Image"
```
-But it should be:
+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 = "PipeSequence"
-definition = "....."
+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 structure.
-- `inputs`: Dictionnary of key behing the variable used in the prompts, and the value behing the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if pipeSequence) or of the conditionnal pipes (if pipeCondition).
+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:
`StaticValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['ocr_input']'``
-That means that the pipe validate_expense is missing the input `ocr_input` because one of the subpipe is needing it.
+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:
@@ -81,53 +83,108 @@ inputs = {
- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-# Structured Models Rules
+### Structuring Models
-## Model Location and Registration
+Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-- Create models for structured generations related to "some_domain" in `pipelex_libraries/pipelines/.py`
-- Models must inherit from `StructuredContent` or appropriate content type
+#### Three Ways to Structure Concepts
-## Model Structure
+**1. No Structure Needed**
-Concepts and their structure classes are meant to indicate an idea.
-A Concept MUST NEVER be a plural noun and you should never create a SomeConceptList: lists and arrays are implicitly handled by Pipelex according to the context. Just define SomeConcept.
+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.
-```python
-from datetime import datetime
-from typing import List, Optional
-from pydantic import Field
+**2. Inline Structure Definition (RECOMMENDED for most cases)**
+
+For concepts with structured fields, define them inline using TOML syntax:
-from pipelex.core.stuffs.stuff_content import StructuredContent
+```plx
+[concept.Invoice]
+description = "A commercial document issued by a seller to a buyer"
+
+[concept.Invoice.structure]
+invoice_number = "The unique invoice identifier"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, default_value = "default" }
+```
-# IMPORTANT: THE CLASS MUST BE A SUBCLASS OF StructuredContent
-class YourModel(StructuredContent): # Always be a subclass of StructuredContent
- # Required fields
- field1: str
- field2: int
+**3. Python StructuredContent Class (For Advanced Features)**
- # Optional fields with defaults
- field3: Optional[str] = Field(None, "Description of field3")
- field4: List[str] = Field(default_factory=list)
+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
- # Date fields should remove timezone
- date_field: Optional[datetime] = None
+```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
```
-## Usage
-Structures are meant to indicate what class to use for a particular Concept. In general they use the same name as the concept.
+**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
+
+#### Decision Rules for Agents
-Structure classes defined within `pipelex_libraries/pipelines/` are automatically loaded into the class_registry when setting up Pipelex, no need to do it manually.
+**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)
-## Best Practices for structures
+**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
-- Respect Pydantic v2 standards
-- Use type hints for all fields
-- Use `Field` declaration and write the description
+#### 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 Operator
+
+### Pipe Controllers and Pipe Operators
Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
@@ -135,24 +192,23 @@ Look at the Pipes we have in order to adapt it. Pipes are organized in two categ
- `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
- - `PipeBatch` - For running pipes in Batch over a ListContent
2. **Operators** - For specific tasks:
- `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeOcr` - OCR Pipe
+ - `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 Guide
+### PipeSequence controller
-## Purpose
-PipeSequence executes multiple pipes in a defined order, where each step can use results from previous steps.
+Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-## Basic Structure
+#### Basic Definition
```plx
[pipe.your_sequence_name]
type = "PipeSequence"
-definition = "Description of what this sequence does"
+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 = [
@@ -162,13 +218,13 @@ steps = [
]
```
-## Key Components
+#### 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
+#### Using PipeBatch in Steps
You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
@@ -189,23 +245,22 @@ steps = [
The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-# PipeCondition Controller
+### 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.
-## Usage in PLX Configuration
-
-### Basic Usage with Direct Expression
+#### Basic usage
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
@@ -214,205 +269,609 @@ or
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
```
-## Key Parameters
+#### 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)
-- `pipe_map`: 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`, ..) is the name of the pipce to trigger
-
-# PipeBatch Controller
-
-The PipeBatch controller allows you to apply a pipe operation to each element in a list of inputs in parallele. It is created via a PipeSequence.
-
-## Usage in PLX Configuration
+- `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.sequence_with_batch]
-type = "PipeSequence"
-definition = "A Sequence of pipes"
-inputs = { input_data = "ConceptName" }
-output = "OutputConceptName"
-steps = [
- { pipe = "pipe_to_apply", batch_over = "input_list", batch_as = "current_item", result = "batch_results" }
-]
-```
-
-## Key Parameters
-
-- `pipe`: The pipe operation to apply to each element in the batch
-- `batch_over`: The name of the list in the context to iterate over
-- `batch_as`: The name to use for the current element in the pipe's context
-- `result`: Where to store the results of the batch operation
+[pipe.strict_validation]
+type = "PipeCondition"
+description = "Validate with strict matching"
+inputs = { status = "Status" }
+output = "Text"
+expression = "status.value"
+default_outcome = "fail"
-# PipeLLM Guide
+[pipe.strict_validation.outcomes]
+approved = "process_approved"
+rejected = "process_rejected"
+```
-## Purpose
+### PipeLLM operator
PipeLLM is used to:
1. Generate text or objects with LLMs
2. Process images with Vision LLMs
-## Basic Usage
+#### Basic Usage
-### Simple Text Generation
+Simple Text Generation:
```plx
[pipe.write_story]
type = "PipeLLM"
-definition = "Write a short story"
+description = "Write a short story"
output = "Text"
-prompt_template = """
+prompt = """
Write a short story about a programmer.
"""
```
-### Structured Data Extraction
+Structured Data Extraction:
```plx
[pipe.extract_info]
type = "PipeLLM"
-definition = "Extract information"
+description = "Extract information"
inputs = { text = "Text" }
output = "PersonInfo"
-prompt_template = """
+prompt = """
Extract person information from this text:
@text
"""
```
-### System Prompts
-Add system-level instructions:
+Supports system instructions:
```plx
[pipe.expert_analysis]
type = "PipeLLM"
-definition = "Expert analysis"
+description = "Expert analysis"
output = "Analysis"
system_prompt = "You are a data analysis expert"
-prompt_template = "Analyze this data"
+prompt = "Analyze this data"
```
-### Multiple Outputs
-Generate multiple results:
+#### Multiple Outputs
+
+Generate multiple outputs (fixed number):
```plx
[pipe.generate_ideas]
type = "PipeLLM"
-definition = "Generate ideas"
+description = "Generate ideas"
output = "Idea"
nb_output = 3 # Generate exactly 3 ideas
-# OR
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
multiple_output = true # Let the LLM decide how many to generate
```
-### Vision Tasks
-Process images with VLMs:
+#### Vision
+
+Process images with VLMs (image inputs must be tagged in the prompt):
```plx
[pipe.analyze_image]
type = "PipeLLM"
-definition = "Analyze image"
-inputs = { image = "Image" } # `image` is the name of the stuff that contains the Image. If its in a stuff, you can add something like `{ "page.image": "Image" }
+description = "Analyze image"
+inputs = { image = "Image" }
output = "ImageAnalysis"
-prompt_template = "Describe what you see in this image"
+prompt = """
+Describe what you see in this image:
+
+$image
+"""
```
-# PipeOCR Guide
+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."
+```
-## Purpose
+#### Writing prompts for PipeLLM
-Extract text and images from an image or a PDF
+**Insert stuff inside a tagged block**
-## Basic Usage
+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.
-### Simple Text Generation
+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 = "PipeOcr"
-definition = "extract the information"
-inputs = { ocr_input = "PDF" } # or { ocr_input = "Image" } if its an image. This is the only input
+type = "PipeExtract"
+description = "extract the information"
+inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
output = "Page"
```
-The input ALWAYS HAS TO BE `ocr_input` and the value is either of concept `Image` or `Pdf`.
+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 PipeOcr is outputing a `ListContent` of `Page`
+It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
```python
class TextAndImagesContent(StuffContent):
- text: Optional[TextContent]
- images: Optional[List[ImageContent]]
+ text: TextContent | None
+ images: list[ImageContent] | None
class PageContent(StructuredContent): # CONCEPT IS "Page"
text_and_images: TextAndImagesContent
- page_view: Optional[ImageContent] = None
+ 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.
-This rule explains how to write prompt templates in PipeLLM definitions.
+### PipeCompose operator
-## Insert stuff inside a tagged block
+The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-If the inserted text is supposedly 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.
+#### Basic Usage
-Example template:
+Simple Template Composition:
```plx
-prompt_template = """
-Match the expense with its corresponding invoice:
+[pipe.compose_report]
+type = "PipeCompose"
+description = "Compose a report using template"
+inputs = { data = "ReportData" }
+output = "Text"
+template = """
+## Report Summary
-@expense
+Based on the analysis:
+$data
-@invoices
+Generated on: {{ current_date }}
"""
```
-In this example, 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 doens't need to be explictly written in the prompt template.
-**DO NOT write things like "Here is the expense: @expense".**
-**DO write simply "@expense" alone in an isolated line.**
+Using Named Templates:
+```plx
+[pipe.use_template]
+type = "PipeCompose"
+description = "Use a predefined template"
+inputs = { content = "Text" }
+output = "Text"
+template_name = "standard_report_template"
+```
-## Insert stuff inline
+Using Nested Template Section (for more control):
+```plx
+[pipe.advanced_template]
+type = "PipeCompose"
+description = "Use advanced template settings"
+inputs = { data = "ReportData" }
+output = "Text"
-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.
+[pipe.advanced_template.template]
+template = "Report: $data"
+category = "html"
+templating_style = { tag_style = "square_brackets", text_format = "html" }
+```
-Example template:
+CRM Email Template:
```plx
-prompt_template = """
-Your goal is to summarize everything related to $topic in the provided text:
+[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
-@text
+Hi $customer.first_name,
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
+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
"""
```
-Here, $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.
+#### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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)
-**DO NOT write "$topic" alone in an isolated line.**
-**DO write things like "Write an essay about $topic" included in an actual sentence.**
+#### Input Requirements
-# Example to execute a pipeline
+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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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.hub import get_pipeline_tracker, get_report_delegate
from pipelex.pipelex import Pipelex
from pipelex.pipeline.execute import execute_pipeline
-from pipelex_libraries.pipelines.examples.extract_gantt.gantt import GanttChart
+
+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"
@@ -433,31 +892,32 @@ async def extract_gantt(image_url: str) -> GanttChart:
return pipe_output.main_stuff_as(content_type=GanttChart)
-# start Pipelex
+## start Pipelex
Pipelex.make()
-# run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(IMAGE_URL))
-
-# Display cost report (tokens used and cost)
-get_report_delegate().generate_report()
-# output results
+## run sample using asyncio
+gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
pretty_print(gantt_chart, title="Gantt Chart")
-get_pipeline_tracker().output_flowchart()
```
-The input memory is a dictionary of key-value pairs, 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:
+### 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
-ImplicitMemory = Dict[str, StuffContentOrData]
+StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
+ImplicitMemory = 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.
+## 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",
input_memory={
@@ -465,18 +925,18 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
- "ocr_input": PDFContent(url=pdf_url),
+ "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:
+## 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",
input_memory={
@@ -484,9 +944,9 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
@@ -497,7 +957,7 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# Here is a more complex example with multiple inputs assigned using different ways:
+## 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",
@@ -512,64 +972,141 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
)
```
-ALWAYS RUN `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
+### Using the outputs of a pipeline
-# Rules to choose LLM models used in PipeLLMs.
+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:
-## LLM Handles
+```python
-In order to use it in a pipe, an LLM is referenced by its llm_handle and possibly by an llm_preset.
-Both llm_handles and llm_presets are defined in this toml config file: [base_llm_deck.toml](mdc:your/path/to/pipelex/config/folder/llm_deck/base_llm_deck.toml)
+class PipeOutput(BaseModel):
+ working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
+ pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-## LLM Handles
+ @property
+ def main_stuff(self) -> Stuff:
+ ...
-An llm_handle matches the handle (an id of sorts) with the full specification of the LLM to use, i.e.:
-- llm_name
-- llm_version
-- llm_platform_choice
+ def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
+ ...
-The declaration of llm_handles looks like this in toml syntax:
-```toml
-[llm_handles]
-gpt-4o-2024-11-20 = { llm_name = "gpt-4o", llm_version = "2024-11-20" }
+ 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:
+ ...
```
-In mosty cases, we only want to use version "latest" and llm_platform_choice "default" in which case the declaration is simply a match of the llm_handle to the llm_name, like this:
+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
-best-claude = "claude-4-opus"
-best-gemini = "gemini-2.5-pro"
-best-mistral = "mistral-large"
+[aliases]
+base-claude = "claude-4.5-sonnet"
+base-gpt = "gpt-5"
+base-gemini = "gemini-2.5-flash"
+base-mistral = "mistral-medium"
```
-And of course, llm_handles are automatically assigned for all models by their name, with version "latest" and llm_platform_choice "default".
+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
+### 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"
-definition = "Write text about Hello World."
+description = "Write text about Hello World."
output = "Text"
-llm = { llm_handle = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
-prompt_template = """
+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
+### 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_reason = { llm_handle = "o4-mini", temperature = 1, max_tokens = "auto" }
-llm_to_extract_invoice = { llm_handle = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
+llm_to_reason = { 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:
@@ -577,11 +1114,11 @@ The interest is that these presets can be used to set the LLM choice in a PipeLL
```plx
[pipe.extract_invoice]
type = "PipeLLM"
-definition = "Extract invoice information from an invoice text transcript"
+description = "Extract invoice information from an invoice text transcript"
inputs = { invoice_text = "InvoiceText" }
output = "Invoice"
-llm = "llm_to_extract_invoice"
-prompt_template = """
+model = "llm_to_extract_invoice"
+prompt = """
Extract invoice information from this invoice:
The category of this invoice is: $invoice_details.category.
@@ -590,8 +1127,9 @@ The category of this invoice is: $invoice_details.category.
"""
```
-The setting here `llm = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
+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 in [overrides.toml](your/path/to/pipelex/config/folder/llm_deck/overrides.toml).
+You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
+
diff --git a/BLACKBOX_RULES.md b/BLACKBOX_RULES.md
new file mode 100644
index 0000000..4d612f7
--- /dev/null
+++ b/BLACKBOX_RULES.md
@@ -0,0 +1,1135 @@
+
+## 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 the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- 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)
+
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. 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:
+`StaticValidationError: 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:
+
+### 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"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, 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):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+nb_output = 3 # Generate exactly 3 ideas
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
+multiple_output = true # Let the LLM decide how many to generate
+```
+
+#### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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",
+ input_memory={
+ "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
+ImplicitMemory = 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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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",
+ input_memory={
+ "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_reason = { 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`.
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6917b1a..b2fec6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# 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
diff --git a/CLAUDE.md b/CLAUDE.md
index 93859f7..4d612f7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,73 +1,75 @@
-# Pipeline Guide
+
+## Guide to write or edit pipelines using the Pipelex language in .plx files
-- Always first write your "plan" in natural langage, then transcribe it in pipelex.
-- You should ALWAYS RUN the terminal command `make validate` when you are writing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
-- Please use POSIX standard for files. (enmpty lines, no trailing whitespaces, etc.)
+- Always first write your "plan" in natural language, then transcribe it in pipelex.
+- You should ALWAYS RUN the terminal command `make validate` when you are writing or editing a `.plx` file. It will ensure the pipe is runnable. If not, iterate.
+- Please use POSIX standard for files. (empty lines, no trailing whitespaces, etc.)
-# Pipeline Structure Guide
-
-## Pipeline File Naming
+### 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 structures
+- Files must be `.py` for code defining the data structures
- Use descriptive names in `snake_case`
-## Pipeline File Structure
+### Pipeline File Outline
A pipeline file has three main sections:
1. Domain statement
2. Concept definitions
3. Pipe definitions
-### Domain Statement
+#### Domain Statement
```plx
domain = "domain_name"
-definition = "Description of the domain" # Optional
+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
+#### 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" # Should be the same name as the Structure ClassName you want to output
+ConceptName = "Description of the concept"
```
-Important Rules:
+**Naming Rules:**
- Use PascalCase for concept names
-- Never use plurals (no "Stories", use "Story")
-- Avoid adjectives (no "LargeText", use "Text")
-- Don't redefine native concepts (Text, Image, PDF, TextAndImages, Number)
-yes
-### Pipe Definitions
+- 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)
-## Pipe Base Structure
+**Native Concepts:**
+Pipelex provides built-in native concepts: `Text`, `Image`, `PDF`, `TextAndImages`, `Number`, `Page`. Use these directly or refine them when appropriate.
-```plx
-[pipe.your_pipe_name]
-type = "PipeLLM"
-definition = "A description of what your pipe does"
-inputs = { input_1 = "ConceptName1", input_2 = "ConceptName2" }
-output = "ConceptName"
-```
+**Refining Native Concepts:**
+To create a concept that specializes a native concept without adding fields:
-DO NOT WRITE:
```plx
-[pipe.your_pipe_name]
-type = "pipe_sequence"
+[concept.Landscape]
+description = "A scenic outdoor photograph"
+refines = "Image"
```
-But it should be:
+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 = "PipeSequence"
-definition = "....."
+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 structure.
-- `inputs`: Dictionnary of key behing the variable used in the prompts, and the value behing the ConceptName. It should ALSO LIST THE INPUTS OF THE INTERMEDIATE STEPS (if pipeSequence) or of the conditionnal pipes (if pipeCondition).
+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:
`StaticValidationError: missing_input_variable • domain='expense_validator' • pipe='validate_expense' •
-variable='['ocr_input']'``
-That means that the pipe validate_expense is missing the input `ocr_input` because one of the subpipe is needing it.
+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:
@@ -81,53 +83,108 @@ inputs = {
- `output`: The name of the concept to output. The `ConceptName` should have the same name as the python class if you want structured output:
-# Structured Models Rules
+### Structuring Models
-## Model Location and Registration
+Once you've defined your concepts semantically (see "Concept Definitions" above), you need to specify their structure if they have fields.
-- Create models for structured generations related to "some_domain" in `pipelex_libraries/pipelines/.py`
-- Models must inherit from `StructuredContent` or appropriate content type
+#### Three Ways to Structure Concepts
-## Model Structure
+**1. No Structure Needed**
-Concepts and their structure classes are meant to indicate an idea.
-A Concept MUST NEVER be a plural noun and you should never create a SomeConceptList: lists and arrays are implicitly handled by Pipelex according to the context. Just define SomeConcept.
+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.
-```python
-from datetime import datetime
-from typing import List, Optional
-from pydantic import Field
+**2. Inline Structure Definition (RECOMMENDED for most cases)**
+
+For concepts with structured fields, define them inline using TOML syntax:
-from pipelex.core.stuffs.stuff_content import StructuredContent
+```plx
+[concept.Invoice]
+description = "A commercial document issued by a seller to a buyer"
+
+[concept.Invoice.structure]
+invoice_number = "The unique invoice identifier"
+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"
+line_items = { type = "list", item_type = "text", description = "List of items", required = false }
+```
+
+**Supported inline field types:** `text`, `integer`, `boolean`, `number`, `date`, `list`, `dict`
+
+**Field properties:** `type`, `description`, `required` (default: true), `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", required = false, default_value = "default" }
+```
-# IMPORTANT: THE CLASS MUST BE A SUBCLASS OF StructuredContent
-class YourModel(StructuredContent): # Always be a subclass of StructuredContent
- # Required fields
- field1: str
- field2: int
+**3. Python StructuredContent Class (For Advanced Features)**
- # Optional fields with defaults
- field3: Optional[str] = Field(None, "Description of field3")
- field4: List[str] = Field(default_factory=list)
+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
- # Date fields should remove timezone
- date_field: Optional[datetime] = None
+```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
```
-## Usage
-Structures are meant to indicate what class to use for a particular Concept. In general they use the same name as the concept.
+**Location:** Create models in `my_project/some_domain/some_domain_struct.py`. Classes inheriting from `StructuredContent` are automatically discovered.
+
+#### Decision Rules for Agents
-Structure classes defined within `pipelex_libraries/pipelines/` are automatically loaded into the class_registry when setting up Pipelex, no need to do it manually.
+**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)
-## Best Practices for structures
+**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
-- Respect Pydantic v2 standards
-- Use type hints for all fields
-- Use `Field` declaration and write the description
+#### 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 Operator
+
+### Pipe Controllers and Pipe Operators
Look at the Pipes we have in order to adapt it. Pipes are organized in two categories:
@@ -135,24 +192,23 @@ Look at the Pipes we have in order to adapt it. Pipes are organized in two categ
- `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
- - `PipeBatch` - For running pipes in Batch over a ListContent
2. **Operators** - For specific tasks:
- `PipeLLM` - Generate Text and Objects (include Vision LLM)
- - `PipeOcr` - OCR Pipe
+ - `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 Guide
+### PipeSequence controller
-## Purpose
-PipeSequence executes multiple pipes in a defined order, where each step can use results from previous steps.
+Purpose: PipeSequence executes multiple pipes in a defined order, where each step can use results from original inputs or from previous steps.
-## Basic Structure
+#### Basic Definition
```plx
[pipe.your_sequence_name]
type = "PipeSequence"
-definition = "Description of what this sequence does"
+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 = [
@@ -162,13 +218,13 @@ steps = [
]
```
-## Key Components
+#### 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
+#### Using PipeBatch in Steps
You can use PipeBatch functionality within steps using `batch_over` and `batch_as`:
@@ -189,23 +245,22 @@ steps = [
The result of a batched step will be a `ListContent` containing the outputs from processing each item.
-# PipeCondition Controller
+### 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.
-## Usage in PLX Configuration
-
-### Basic Usage with Direct Expression
+#### Basic usage
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
@@ -214,205 +269,609 @@ or
```plx
[pipe.conditional_operation]
type = "PipeCondition"
-definition = "A conditonal pipe to decide wheter..."
+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.pipe_map]
+[pipe.conditional_operation.outcomes]
small = "process_small"
medium = "process_medium"
large = "process_large"
```
-## Key Parameters
+#### 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)
-- `pipe_map`: 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`, ..) is the name of the pipce to trigger
-
-# PipeBatch Controller
-
-The PipeBatch controller allows you to apply a pipe operation to each element in a list of inputs in parallele. It is created via a PipeSequence.
-
-## Usage in PLX Configuration
+- `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.sequence_with_batch]
-type = "PipeSequence"
-definition = "A Sequence of pipes"
-inputs = { input_data = "ConceptName" }
-output = "OutputConceptName"
-steps = [
- { pipe = "pipe_to_apply", batch_over = "input_list", batch_as = "current_item", result = "batch_results" }
-]
-```
-
-## Key Parameters
-
-- `pipe`: The pipe operation to apply to each element in the batch
-- `batch_over`: The name of the list in the context to iterate over
-- `batch_as`: The name to use for the current element in the pipe's context
-- `result`: Where to store the results of the batch operation
+[pipe.strict_validation]
+type = "PipeCondition"
+description = "Validate with strict matching"
+inputs = { status = "Status" }
+output = "Text"
+expression = "status.value"
+default_outcome = "fail"
-# PipeLLM Guide
+[pipe.strict_validation.outcomes]
+approved = "process_approved"
+rejected = "process_rejected"
+```
-## Purpose
+### PipeLLM operator
PipeLLM is used to:
1. Generate text or objects with LLMs
2. Process images with Vision LLMs
-## Basic Usage
+#### Basic Usage
-### Simple Text Generation
+Simple Text Generation:
```plx
[pipe.write_story]
type = "PipeLLM"
-definition = "Write a short story"
+description = "Write a short story"
output = "Text"
-prompt_template = """
+prompt = """
Write a short story about a programmer.
"""
```
-### Structured Data Extraction
+Structured Data Extraction:
```plx
[pipe.extract_info]
type = "PipeLLM"
-definition = "Extract information"
+description = "Extract information"
inputs = { text = "Text" }
output = "PersonInfo"
-prompt_template = """
+prompt = """
Extract person information from this text:
@text
"""
```
-### System Prompts
-Add system-level instructions:
+Supports system instructions:
```plx
[pipe.expert_analysis]
type = "PipeLLM"
-definition = "Expert analysis"
+description = "Expert analysis"
output = "Analysis"
system_prompt = "You are a data analysis expert"
-prompt_template = "Analyze this data"
+prompt = "Analyze this data"
```
-### Multiple Outputs
-Generate multiple results:
+#### Multiple Outputs
+
+Generate multiple outputs (fixed number):
```plx
[pipe.generate_ideas]
type = "PipeLLM"
-definition = "Generate ideas"
+description = "Generate ideas"
output = "Idea"
nb_output = 3 # Generate exactly 3 ideas
-# OR
+```
+
+Generate multiple outputs (variable number):
+```plx
+[pipe.generate_ideas]
+type = "PipeLLM"
+description = "Generate ideas"
+output = "Idea"
multiple_output = true # Let the LLM decide how many to generate
```
-### Vision Tasks
-Process images with VLMs:
+#### Vision
+
+Process images with VLMs (image inputs must be tagged in the prompt):
```plx
[pipe.analyze_image]
type = "PipeLLM"
-definition = "Analyze image"
-inputs = { image = "Image" } # `image` is the name of the stuff that contains the Image. If its in a stuff, you can add something like `{ "page.image": "Image" }
+description = "Analyze image"
+inputs = { image = "Image" }
output = "ImageAnalysis"
-prompt_template = "Describe what you see in this image"
+prompt = """
+Describe what you see in this image:
+
+$image
+"""
```
-# PipeOCR Guide
+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."
+```
-## Purpose
+#### Writing prompts for PipeLLM
-Extract text and images from an image or a PDF
+**Insert stuff inside a tagged block**
-## Basic Usage
+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.
-### Simple Text Generation
+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 = "PipeOcr"
-definition = "extract the information"
-inputs = { ocr_input = "PDF" } # or { ocr_input = "Image" } if its an image. This is the only input
+type = "PipeExtract"
+description = "extract the information"
+inputs = { document = "PDF" } # or { image = "Image" } if it's an image. This is the only input.
output = "Page"
```
-The input ALWAYS HAS TO BE `ocr_input` and the value is either of concept `Image` or `Pdf`.
+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 PipeOcr is outputing a `ListContent` of `Page`
+It corresponds to 1 page. Therefore, the PipeExtract is outputing a `ListContent` of `Page`
```python
class TextAndImagesContent(StuffContent):
- text: Optional[TextContent]
- images: Optional[List[ImageContent]]
+ text: TextContent | None
+ images: list[ImageContent] | None
class PageContent(StructuredContent): # CONCEPT IS "Page"
text_and_images: TextAndImagesContent
- page_view: Optional[ImageContent] = None
+ 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.
-This rule explains how to write prompt templates in PipeLLM definitions.
+### PipeCompose operator
-## Insert stuff inside a tagged block
+The PipeCompose operator is used to compose text using Jinja2 templates. It supports various output formats including HTML, Markdown, Mermaid diagrams, and more.
-If the inserted text is supposedly 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.
+#### Basic Usage
-Example template:
+Simple Template Composition:
```plx
-prompt_template = """
-Match the expense with its corresponding invoice:
+[pipe.compose_report]
+type = "PipeCompose"
+description = "Compose a report using template"
+inputs = { data = "ReportData" }
+output = "Text"
+template = """
+## Report Summary
-@expense
+Based on the analysis:
+$data
-@invoices
+Generated on: {{ current_date }}
"""
```
-In this example, 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 doens't need to be explictly written in the prompt template.
-**DO NOT write things like "Here is the expense: @expense".**
-**DO write simply "@expense" alone in an isolated line.**
+Using Named Templates:
+```plx
+[pipe.use_template]
+type = "PipeCompose"
+description = "Use a predefined template"
+inputs = { content = "Text" }
+output = "Text"
+template_name = "standard_report_template"
+```
-## Insert stuff inline
+Using Nested Template Section (for more control):
+```plx
+[pipe.advanced_template]
+type = "PipeCompose"
+description = "Use advanced template settings"
+inputs = { data = "ReportData" }
+output = "Text"
-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.
+[pipe.advanced_template.template]
+template = "Report: $data"
+category = "html"
+templating_style = { tag_style = "square_brackets", text_format = "html" }
+```
-Example template:
+CRM Email Template:
```plx
-prompt_template = """
-Your goal is to summarize everything related to $topic in the provided text:
+[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
-@text
+Hi $customer.first_name,
-Please provide only the summary, with no additional text or explanations.
-Your summary should not be longer than 2 sentences.
+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
"""
```
-Here, $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.
+#### 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"
+nb_output = 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:**
+- `nb_output`: Number of images to generate
+- `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)
-**DO NOT write "$topic" alone in an isolated line.**
-**DO write things like "Write an essay about $topic" included in an actual sentence.**
+#### Input Requirements
-# Example to execute a pipeline
+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_reason = { 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 `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
+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.hub import get_pipeline_tracker, get_report_delegate
from pipelex.pipelex import Pipelex
from pipelex.pipeline.execute import execute_pipeline
-from pipelex_libraries.pipelines.examples.extract_gantt.gantt import GanttChart
+
+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"
@@ -433,31 +892,32 @@ async def extract_gantt(image_url: str) -> GanttChart:
return pipe_output.main_stuff_as(content_type=GanttChart)
-# start Pipelex
+## start Pipelex
Pipelex.make()
-# run sample using asyncio
-gantt_chart = asyncio.run(extract_gantt(IMAGE_URL))
-
-# Display cost report (tokens used and cost)
-get_report_delegate().generate_report()
-# output results
+## run sample using asyncio
+gantt_chart = asyncio.run(extract_gantt(image_url=IMAGE_URL))
pretty_print(gantt_chart, title="Gantt Chart")
-get_pipeline_tracker().output_flowchart()
```
-The input memory is a dictionary of key-value pairs, 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:
+### 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
-ImplicitMemory = Dict[str, StuffContentOrData]
+StuffContentOrData = dict[str, Any] | StuffContent | list[Any] | str
+ImplicitMemory = 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.
+## 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",
input_memory={
@@ -465,18 +925,18 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
- "ocr_input": PDFContent(url=pdf_url),
+ "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:
+## 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",
input_memory={
@@ -484,9 +944,9 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# 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:
+## 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",
input_memory={
@@ -497,7 +957,7 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
},
)
-# Here is a more complex example with multiple inputs assigned using different ways:
+## 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",
@@ -512,64 +972,141 @@ So here are a few concrete examples of calls to execute_pipeline with various wa
)
```
-ALWAYS RUN `make validate` when you are finished writing pipelines: This checks for errors. If there are errors, iterate until it works.
-Then, create an example file to run the pipeline in the `examples` folder.
-But don't write documentation unless asked explicitly to.
+### Using the outputs of a pipeline
-# Rules to choose LLM models used in PipeLLMs.
+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:
-## LLM Handles
+```python
-In order to use it in a pipe, an LLM is referenced by its llm_handle and possibly by an llm_preset.
-Both llm_handles and llm_presets are defined in this toml config file: [base_llm_deck.toml](mdc:your/path/to/pipelex/config/folder/llm_deck/base_llm_deck.toml)
+class PipeOutput(BaseModel):
+ working_memory: WorkingMemory = Field(default_factory=WorkingMemory)
+ pipeline_run_id: str = Field(default=SpecialPipelineId.UNTITLED)
-## LLM Handles
+ @property
+ def main_stuff(self) -> Stuff:
+ ...
-An llm_handle matches the handle (an id of sorts) with the full specification of the LLM to use, i.e.:
-- llm_name
-- llm_version
-- llm_platform_choice
+ def main_stuff_as_list(self, item_type: type[StuffContentType]) -> ListContent[StuffContentType]:
+ ...
-The declaration of llm_handles looks like this in toml syntax:
-```toml
-[llm_handles]
-gpt-4o-2024-11-20 = { llm_name = "gpt-4o", llm_version = "2024-11-20" }
+ 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:
+ ...
```
-In mosty cases, we only want to use version "latest" and llm_platform_choice "default" in which case the declaration is simply a match of the llm_handle to the llm_name, like this:
+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
-best-claude = "claude-4-opus"
-best-gemini = "gemini-2.5-pro"
-best-mistral = "mistral-large"
+[aliases]
+base-claude = "claude-4.5-sonnet"
+base-gpt = "gpt-5"
+base-gemini = "gemini-2.5-flash"
+base-mistral = "mistral-medium"
```
-And of course, llm_handles are automatically assigned for all models by their name, with version "latest" and llm_platform_choice "default".
+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
+### 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"
-definition = "Write text about Hello World."
+description = "Write text about Hello World."
output = "Text"
-llm = { llm_handle = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
-prompt_template = """
+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
+### 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_reason = { llm_handle = "o4-mini", temperature = 1, max_tokens = "auto" }
-llm_to_extract_invoice = { llm_handle = "claude-3-7-sonnet", temperature = 0.1, max_tokens = "auto" }
+llm_to_reason = { 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:
@@ -577,11 +1114,11 @@ The interest is that these presets can be used to set the LLM choice in a PipeLL
```plx
[pipe.extract_invoice]
type = "PipeLLM"
-definition = "Extract invoice information from an invoice text transcript"
+description = "Extract invoice information from an invoice text transcript"
inputs = { invoice_text = "InvoiceText" }
output = "Invoice"
-llm = "llm_to_extract_invoice"
-prompt_template = """
+model = "llm_to_extract_invoice"
+prompt = """
Extract invoice information from this invoice:
The category of this invoice is: $invoice_details.category.
@@ -590,8 +1127,9 @@ The category of this invoice is: $invoice_details.category.
"""
```
-The setting here `llm = "llm_to_extract_invoice"` works because "llm_to_extract_invoice" has been declared as an llm_preset in the deck.
+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 in [overrides.toml](your/path/to/pipelex/config/folder/llm_deck/overrides.toml).
+You can override the predefined llm presets by setting them in `.pipelex/inference/deck/overrides.toml`.
+
diff --git a/Makefile b/Makefile
index 0cf8918..28e50a0 100644
--- a/Makefile
+++ b/Makefile
@@ -42,6 +42,10 @@ make env - Create python virtual env
make lock - Refresh uv.lock without updating anything
make install - Create local virtualenv & install all dependencies
make update - Upgrade dependencies via uv
+make export-requirements - Export production requirements.txt (no dev dependencies)
+make export-requirements-dev - Export requirements-dev.txt (all dependencies including dev)
+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
@@ -51,9 +55,7 @@ make mypy - Check types with mypy
make cleanenv - Remove virtual env and lock files
make cleanderived - Remove extraneous compiled files, caches, logs, etc.
-make cleanlibraries - Remove pipelex_libraries
-make cleanall - Remove all -> cleanenv + cleanderived + cleanlibraries
-make reinitbaselibrary - Remove pipelex_libraries and init libraries again
+make cleanall - Remove all -> cleanenv + cleanderived
make reinstall - Reinstall dependencies
make merge-check-ruff-lint - Run ruff merge check without updating files
@@ -61,20 +63,17 @@ make merge-check-ruff-format - Run ruff merge check without updating files
make merge-check-mypy - Run mypy merge check without updating files
make merge-check-pyright - Run pyright merge check without updating files
-make rl - Shorthand -> reinitlibraries
make ri - Shorthand -> reinstall
make v - Shorthand -> validate
-make init - Run pipelex init
make codex-tests - Run tests for Codex (exit on first failure) (no inference, no codex_disabled)
make gha-tests - Run tests for github actions (exit on first failure) (no inference, no gha_disabled)
make test - Run unit tests (no inference)
make test-with-prints - Run tests with prints (no inference)
make t - Shorthand -> test-with-prints
make tp - Shorthand -> test-with-prints
+make tb - Shorthand -> `make test-with-prints TEST=test_boot`
make test-inference - Run unit tests only for inference (with prints)
make ti - Shorthand -> test-inference
-make test-imgg - Run unit tests only for imgg (with prints)
-make test-g - Shorthand -> test-imgg
make check - Shorthand -> format lint mypy
make c - Shorthand -> check
@@ -89,8 +88,9 @@ export HELP
.PHONY: \
all help env lock install update build \
+ export-requirements export-requirements-dev er erd \
format lint pyright mypy \
- cleanderived cleanenv cleanlibraries cleanall \
+ 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 \
@@ -123,18 +123,12 @@ env: check-uv
echo "Python virtual env already exists in \`${VIRTUAL_ENV}\`"; \
fi
-init: env
- $(call PRINT_TITLE,"Running pipelex init")
- $(VENV_PIPELEX) init libraries
- $(VENV_PIPELEX) init config
-
install: env
$(call PRINT_TITLE,"Installing dependencies")
@. $(VIRTUAL_ENV)/bin/activate && \
uv sync --all-extras && \
- $(VENV_PIPELEX) init libraries && \
$(VENV_PIPELEX) init config && \
- echo "Installed dependencies in ${VIRTUAL_ENV} and initialized Pipelex libraries";
+ echo "Installed dependencies in ${VIRTUAL_ENV} and initialized Pipelex config";
lock: env
$(call PRINT_TITLE,"Resolving dependencies without update")
@@ -147,6 +141,22 @@ update: env
uv pip install -e ".[dev]" && \
echo "Updated dependencies in ${VIRTUAL_ENV}";
+export-requirements: env
+ $(call PRINT_TITLE,"Exporting production requirements")
+ @uv export --no-dev --output-file requirements.txt && \
+ echo "Exported production requirements to requirements.txt";
+
+export-requirements-dev: env
+ $(call PRINT_TITLE,"Exporting development requirements")
+ @uv export --all-extras --output-file requirements-dev.txt && \
+ echo "Exported all requirements (including dev) to requirements-dev.txt";
+
+er: export-requirements
+ @echo "> done: er = export-requirements"
+
+erd: export-requirements-dev
+ @echo "> done: erd = export-requirements-dev"
+
validate: env
$(call PRINT_TITLE,"Running setup sequence")
$(VENV_PIPELEX) validate all
@@ -180,34 +190,18 @@ cleanlock:
@find . -name 'requirements.lock' -delete && \
echo "Cleaned up uv lock file";
-cleanbaselibrary:
- $(call PRINT_TITLE,"Erasing derived files and directories")
- @find . -type d -wholename './pipelex_libraries/pipelines/base_library' -exec rm -rf {} + && \
- echo "Cleaned up pipelex base library";
-
-reinitbaselibrary: cleanbaselibrary init
- @echo "Reinitialized pipelex base library";
-
-rl: reinitbaselibrary
- @echo "> done: rl = reinitlibraries"
-
reinstall: cleanenv cleanlock install
@echo "Reinstalled dependencies";
ri: reinstall
@echo "> done: ri = reinstall"
-cleanlibraries:
- $(call PRINT_TITLE,"Erasing pipelex_libraries")
- @find . -type d -wholename './pipelex_libraries' -exec rm -rf {} + && \
- echo "Cleaned up pipelex_libraries";
-
cleanconfig:
$(call PRINT_TITLE,"Erasing .pipelex config files and directories")
@find . -type d -wholename './.pipelex' -exec rm -rf {} + && \
echo "Cleaned up .pipelex";
-cleanall: cleanderived cleanenv cleanlibraries cleanconfig
+cleanall: cleanderived cleanenv cleanconfig
@echo "Cleaned up all derived files and directories";
##########################################################################################
@@ -275,6 +269,12 @@ t: test-with-prints
tp: test-with-prints
@echo "> done: tp = test-with-prints"
+tb: env
+ $(call PRINT_TITLE,"Unit testing a simple boot")
+ @echo "• Running unit test test_boot"
+ $(VENV_PYTEST) -s -m $(USUAL_PYTEST_MARKERS) -k "test_boot" $(if $(filter 1,$(VERBOSE)),-v,$(if $(filter 2,$(VERBOSE)),-vv,$(if $(filter 3,$(VERBOSE)),-vvv,)));
+
+
test-inference: env
$(call PRINT_TITLE,"Unit testing")
@if [ -n "$(TEST)" ]; then \
@@ -300,8 +300,7 @@ lint: env
pyright: env
$(call PRINT_TITLE,"Typechecking with pyright")
- $(VENV_PYRIGHT) --pythonpath $(VENV_PYTHON) && \
- echo "Done typechecking with pyright — disregard warning about latest version, it's giving us false positives"
+ $(VENV_PYRIGHT) --pythonpath $(VENV_PYTHON) --project pyproject.toml
mypy: env
$(call PRINT_TITLE,"Typechecking with mypy")
@@ -337,16 +336,16 @@ check-unused-imports: env
$(call PRINT_TITLE,"Checking for unused imports without fixing")
$(VENV_RUFF) check --select=F401 --no-fix .
-c: init format lint pyright mypy
+c: format lint pyright mypy
@echo "> done: c = check"
-cc: init cleanderived c
+cc: cleanderived c
@echo "> done: cc = cleanderived check"
-check: init cleanderived check-unused-imports c
+check: cleanderived check-unused-imports c
@echo "> done: check"
-v: init validate
+v: validate
@echo "> done: v = validate"
li: lock install
diff --git a/README.md b/README.md
index 9919779..4d1b448 100644
--- a/README.md
+++ b/README.md
@@ -15,26 +15,18 @@ Once you've created your repository from it, then you can clone it and follow th
---
-### Create virtual environment, install Pipelex and other dependencies
+## Getting Started
-```bash
-make install
-```
-
-This will install the Pipelex python library and its dependencies using uv.
+### Quick Start
-### Set up environment variables
+The fastest way to get started (on Unix/MacOS/Linux):
```bash
-cp .env.example .env
+make install
+cp .env.example .env # Then add your API keys
```
-Enter your API keys into your `.env` file. The `OPENAI_API_KEY` is enough to get you started, but some pipelines require models from other providers.
-
-
-### IDE extension
-
-We **highly** recommend installing our own extension for PLX files into your IDE of choice. You can find it in the [Open VSX Registry](https://open-vsx.org/extension/Pipelex/pipelex). It's coming soon to VS Code marketplace too and if you are using Cursor, Windsurf or another VS Code fork, you can search for it directly in your extensions tab.
+For detailed installation instructions, API key configuration options, and alternative setup methods, see **[PIPELEX_SETUP.md](PIPELEX_SETUP.md)**.
---
diff --git a/pipelex_libraries/pipelines/hello_world.plx b/my_project/hello_world.plx
similarity index 61%
rename from pipelex_libraries/pipelines/hello_world.plx
rename to my_project/hello_world.plx
index 63574c1..504b80c 100644
--- a/pipelex_libraries/pipelines/hello_world.plx
+++ b/my_project/hello_world.plx
@@ -1,14 +1,14 @@
domain = "quick_start"
-definition = "Discovering Pipelex"
+description = "Discovering Pipelex"
[pipe]
[pipe.hello_world]
type = "PipeLLM"
description = "Write text about Hello World."
output = "Text"
-llm = { llm_handle = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
+model = { model = "gpt-4o-mini", temperature = 0.9, max_tokens = "auto" }
prompt = """
Write a haiku about Hello World.
"""
diff --git a/pipelex_libraries/__init__.py b/pipelex_libraries/__init__.py
deleted file mode 100644
index 8b13789..0000000
--- a/pipelex_libraries/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/pipelex_libraries/pipelines/__init__.py b/pipelex_libraries/pipelines/__init__.py
deleted file mode 100644
index 8b13789..0000000
--- a/pipelex_libraries/pipelines/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/pyproject.toml b/pyproject.toml
index fbc0a85..519158f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "my-project"
-version = "0.2.4"
+version = "0.3.0"
description = "Replace this with your project description"
# authors = [{ name = "Your Name", email = "your.email@example.com" }]
license = "MIT"
@@ -16,27 +16,25 @@ classifiers = [
"License :: OSI Approved :: MIT License",
]
-dependencies = ["pipelex[mistralai,anthropic,google,bedrock,fal]==0.10.2"]
+dependencies = [
+ "pipelex[mistralai,anthropic,google,google-genai,bedrock,fal] @ git+https://github.com/Pipelex/pipelex.git",
+]
[project.optional-dependencies]
dev = [
- "cocode==0.1.2",
"boto3-stubs>=1.35.24",
"mypy>=1.11.2",
- "pyright==1.1.398",
+ "pyright>=1.1.405",
"pytest>=8.3.3",
"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-beautifulsoup4>=4.12.0.20240907",
"types-markdown>=3.6.0.20240316",
"types-networkx>=3.3.0.20241020",
"types-openpyxl>=3.1.5.20250306",
- "types-requests>=2.32.0.2024091",
"types-PyYAML>=6.0.12.20250326",
- "types-toml>=0.10.8.20240310",
]
[project.urls]
@@ -62,7 +60,7 @@ ignore_missing_imports = true
module = ["json2html"]
[tool.pyright]
-include = ["my_project"]
+include = ["my_project", "tests"]
analyzeUnannotatedFunctions = true
deprecateTypingAliases = false
disableBytesTypePromotions = true
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..789f55a
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,1668 @@
+# This file was autogenerated by uv via the following command:
+# uv export --all-extras --output-file requirements-dev.txt
+aioboto3==14.3.0 \
+ --hash=sha256:1d18f88bb56835c607b62bb6cb907754d717bedde3ddfff6935727cb48a80135 \
+ --hash=sha256:aec5de94e9edc1ffbdd58eead38a37f00ddac59a519db749a910c20b7b81bca7
+ # via pipelex
+aiobotocore==2.22.0 \
+ --hash=sha256:11091477266b75c2b5d28421c1f2bc9a87d175d0b8619cb830805e7a113a170b \
+ --hash=sha256:b4e6306f79df9d81daff1f9d63189a2dbee4b77ce3ab937304834e35eaaeeccf
+ # via aioboto3
+aiofiles==23.2.1 \
+ --hash=sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107 \
+ --hash=sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a
+ # via
+ # aioboto3
+ # pipelex
+aiohappyeyeballs==2.6.1 \
+ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
+ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
+ # via aiohttp
+aiohttp==3.12.6 \
+ --hash=sha256:012ea107092d4465aeeb681d5b2fb8b51a847a72f0b71906f40876419fba1355 \
+ --hash=sha256:06f20adcdc4f383aeb7ce884705faea44c0376cde5cdee4d32ef62d6cb1f97cc \
+ --hash=sha256:128603479bf13479661d763e77e254139f066914227b5f2ff3284d19e416ad75 \
+ --hash=sha256:148ffa6b2b825ff8520844ce23df9e2a5b969bb6917c4e35a832fbaa025d260d \
+ --hash=sha256:15817882d25e840aba85d1f5706a7128350b81050f8ca9dabfc25a5f521a792c \
+ --hash=sha256:24d19cbd1d21d207ee855500d2033f1852b4d2113a741246ff62eb16a3921306 \
+ --hash=sha256:259269870d9783de87c0430760b2498b770201ead3e11ee86761d268ce5d196a \
+ --hash=sha256:25dac87ee297e2b5826ce8e96c7615ebe7a1613856b1614a207e3376b776021b \
+ --hash=sha256:25de52753386b0c16d5acd2153e7819f52c9e7fc05f5eca804adc174e99b735d \
+ --hash=sha256:2a74a566872f41247774980334e5b0309dac11b402e188bde6db8a57de4506cd \
+ --hash=sha256:2e4fb0d7f221c36ed8469c1d2d9a2bb6a27b543cf90aa46ca701f63fb83dd7ed \
+ --hash=sha256:30511c5e66ac4399d46b4bec57a3d56bc16cfb649255fa798ee95d8b45f97a4b \
+ --hash=sha256:3331ef09dd775302aa5f4d3170bd46659ad018843fab3656f5e72e3ff68df21f \
+ --hash=sha256:37b1c6034a1e14764adad1829cd710543b1699d7985e1d336f0aa52a2dd76ba9 \
+ --hash=sha256:38af291559401d13eb90259ba79ef6ac537ae6b5bdb1251604606a88cd0fd5e0 \
+ --hash=sha256:3cc06a99e065ed7e766d2cd574671428261c1b8f30fedfbd91ab3c738fd9c08d \
+ --hash=sha256:52ce7e90ee9dd25bcd2ed4513e650cc4f9a03bef07a39193b82fb58892004bd6 \
+ --hash=sha256:545f89c389a47bac024655b5676658f35f80b0d007e4c3c7ff865d9aa3bf343a \
+ --hash=sha256:561f545dc062e6c31fc53535d8584c06516bda2fc37821a67a61b69202061e71 \
+ --hash=sha256:58f79b376a426961418df1d08656ec3a01494b7ba81824ae629e6636deddfff7 \
+ --hash=sha256:59e19517abef2af49cff79b8a863497036ff401051c79d6a3b6149a48213a7be \
+ --hash=sha256:5b700cf48fd04b4328965d1afe01f835fe6cdecc3b85ca2d950431e5cc0647f7 \
+ --hash=sha256:5fe1d74ab6cd1f16c3c2f0e3c3230481dcedc0d3ad9f0b82b1e43f44a4980aca \
+ --hash=sha256:6860351cfba0196db2edc387cfeddaf1dae443e55f261ea2bcb77fecb33aae34 \
+ --hash=sha256:6ca81cb1e41d251cc193164409c0bbb0175e696a9997491a10db9171a2f70603 \
+ --hash=sha256:71905d34b3bb1a6be44e986f08404987bb317d890746e71f320cd10cf3222b46 \
+ --hash=sha256:7487f707a4b8167394f6afefa690198300d8a618505583eb536b92202bdec24d \
+ --hash=sha256:77ba53286c89486e8b02fb47352a5a8270bab1084e2a43fe8e35eb261befda13 \
+ --hash=sha256:7d162c4f87f9dcdc7151f6329438de96beb527820381e3159ce08544c57e9ced \
+ --hash=sha256:7f22a0d9a995c12bb20247334b414edaf65ce8f22a1e838b90210238f9b57571 \
+ --hash=sha256:86fb0a5762f936606dcab1ca248f5053587a598ed44825f4744ce3c53ae9a2e9 \
+ --hash=sha256:8885da8ae99bbe6ce43b79e284ef8e6bc5285dea297fe2a163552f09435c8069 \
+ --hash=sha256:8a88046a5adddf5d99f15a1920f6b8f659f46a4cfb5bfabbd668d06df045df7a \
+ --hash=sha256:8ea77675818fd8cac28491d0d59582e5e2e5b14dbf5e21bef797aa5b23b5ca8b \
+ --hash=sha256:938afd243c9ee76a6d78fad10ecca14b88b48b71553e0e9c74b8098efff5ddf8 \
+ --hash=sha256:93a0887cea23f76e9354235b0e79b3c9922ad66529e11637940b6439849105cb \
+ --hash=sha256:93f207a64989346bbd0a9d3b31ebaa3934ea6e0242b555491af7eb97ad1c0a5a \
+ --hash=sha256:9aecb4ce110c9d321860a00b4f9ec72bef691d045f54c983fa678606f3f918b0 \
+ --hash=sha256:9dd9211229fa2f474da01d42fafff196f607a63aaf12d8b34928c43a713eb6d5 \
+ --hash=sha256:a057680218430231eb6ab644d166b7ef398b3ffbac0232f4f789cdce9391400e \
+ --hash=sha256:a1532ea3f41a818d4f50db96306a1975bf31f29787802bec4c63c58f61b6e682 \
+ --hash=sha256:a2f3c974874bd0c76dfdcc60db5a6f96ca023a85318a5ac401603baa7e299272 \
+ --hash=sha256:a52aa39eb1160775a6e80e3025c990e8872c8927c5dd4b51304788bc149b9549 \
+ --hash=sha256:a90b6f2d5ca4d3ad56034863237b59b4a5fab270eb6d11b5c0326b4501448b51 \
+ --hash=sha256:aac87d78f55057ab48ddcc43055620546d40bbc0888d2658d8705d183c98f901 \
+ --hash=sha256:b2e026a9f9ac0df70f14ca5dcaf1f83a55b678e51aa6515d710dd879d2691fd7 \
+ --hash=sha256:bc4be1d8d68a62859f74f9ada9e174791895366601ce66342f54478d3518c8b3 \
+ --hash=sha256:c05776d1854ae9d8132d7ced7ac0067f602d66589797788ed3902d5c68686db5 \
+ --hash=sha256:c1d8a4a5a7e28d8b9ec815ffecca8712b71130a4eee1c5b45e9f2cc4975f3f7c \
+ --hash=sha256:c232720190ca4240c15abefc7b765e987ef88df44d2384612890db87b33898f3 \
+ --hash=sha256:c88ed8c54f7fd6102ef711d24710454707cde4bb3ffdec09982dcb3cb966a3e1 \
+ --hash=sha256:cdb03da5ecf74a331511604f3cf91563bf29127eabb28f4e16d390a73cb826da \
+ --hash=sha256:ce6673b73352edb17c2db86a9586dc7744e0b5009709152a1e75379f16af19e0 \
+ --hash=sha256:cfbf8ed94b57e3b5a886bfe2a530c8eb067064cc4419fd94431a2cbeeddec54c \
+ --hash=sha256:d557918fefb29884335e1a257df6c961f35ba1caf8eddaabad762b3436cf87ff \
+ --hash=sha256:d590b36c3497ecfba4aca71ab9342fb2c07e1b69baf4e28ad4227440c128bb22 \
+ --hash=sha256:d5f698e7b5b57aa4dc646c8f13ccd965c694199595d7a45cecefaf0e5c392890 \
+ --hash=sha256:d7ff55a38fc9851fa5cff41b30605534dfe4d57d02f79447abfed01499fe31d3 \
+ --hash=sha256:d83ab494eb583ba691af9d4d7c073987526bb9f73aa5a19907258ef3a1e39e8a \
+ --hash=sha256:da073f88270aa434ef16a78c21a4269c96c68badc2b9ad5011fa175c06143eee \
+ --hash=sha256:db5c402ea0aed10af2e54e5946bf32f3ebb02a7604eaaa4c41a608053889de4a \
+ --hash=sha256:de83f567e31418fd7bc22c5a03526a2b0a82e68c7a7fec23ef91a398228f559b \
+ --hash=sha256:deddf6b1c83ce518a156b7597a0d7a1a7ec5c1d2c973ba3f1a23f18fa2b7d65e \
+ --hash=sha256:e5c6869319c0a5f4150959e065c40836b18a99e02493c3b4c73b25378aa0f0cc \
+ --hash=sha256:e8da054804352e974f4349fb871b07c8ffa1978e64cfb455e88fbe6fbe4d6dcb \
+ --hash=sha256:ed4db015494a6d0acaadce035531f9fb321afab2075a4b348811e4f7795e87e6 \
+ --hash=sha256:eefd98dd043c33c45123c56a79c6c39acb628304337c90f16f33569cc3aa4ba6 \
+ --hash=sha256:efbbde2297e4ab10d187103aba9b565277c85ac7d24d98cae201c033ce885504 \
+ --hash=sha256:fd1d6116c1364ab00ffed1654a01091dc7f897d315c5103bcc6e5ab7f70172c7
+ # via
+ # aiobotocore
+ # instructor
+aioitertools==0.12.0 \
+ --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \
+ --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796
+ # via aiobotocore
+aiosignal==1.3.2 \
+ --hash=sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5 \
+ --hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54
+ # via aiohttp
+annotated-types==0.7.0 \
+ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
+ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
+ # via pydantic
+anthropic==0.52.1 \
+ --hash=sha256:807cee7ebc5503753da0403a77932decf5a4c036041ddda58b4edcdb2a3da551 \
+ --hash=sha256:da4a7c3aeac0170cb45a42dc3369ca1fcf2b3238edf845cb056505d4b0c42fcf
+ # via pipelex
+anyio==4.9.0 \
+ --hash=sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028 \
+ --hash=sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c
+ # via
+ # anthropic
+ # httpx
+ # openai
+async-timeout==5.0.1 ; python_full_version < '3.11' \
+ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
+ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
+ # via aiohttp
+attrs==25.3.0 \
+ --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \
+ --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b
+ # via aiohttp
+backports-strenum==1.3.1 ; python_full_version < '3.11' \
+ --hash=sha256:77c52407342898497714f0596e86188bb7084f89063226f4ba66863482f42414 \
+ --hash=sha256:cdcfe36dc897e2615dc793b7d3097f54d359918fc448754a517e6f23044ccf83
+ # via pipelex
+boto3==1.37.3 \
+ --hash=sha256:2063b40af99fd02f6228ff52397b552ff3353831edaf8d25cc04801827ab9794 \
+ --hash=sha256:21f3ce0ef111297e63a6eb998a25197b8c10982970c320d4c6e8db08be2157be
+ # via
+ # aiobotocore
+ # pipelex
+boto3-stubs==1.38.27 \
+ --hash=sha256:87e6c47b76173df91f55387a91fd1f382f130821e4492e14748fe49d46598bc5 \
+ --hash=sha256:8a45f32b83d29ec3db28b37ea68ea5f179b141863211497ef92ba18e2a896aec
+ # via my-project
+botocore==1.37.3 \
+ --hash=sha256:d01bd3bf4c80e61fa88d636ad9f5c9f60a551d71549b481386c6b4efe0bb2b2e \
+ --hash=sha256:fe8403eb55a88faf9b0f9da6615e5bee7be056d75e17af66c3c8f0a3b0648da4
+ # via
+ # aiobotocore
+ # boto3
+ # s3transfer
+botocore-stubs==1.38.27 \
+ --hash=sha256:408158a72199d0611b6c91043e3d654171e974b89b94007cc10488f192e0a20f \
+ --hash=sha256:58315d1318b79d8f8bd2888b1587d90fa4ffa6100a58b610946110e88570b995
+ # via
+ # boto3-stubs
+ # types-aioboto3
+ # types-aiobotocore
+cachetools==5.5.2 \
+ --hash=sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4 \
+ --hash=sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a
+ # via google-auth
+certifi==2025.4.26 \
+ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
+ --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
+ # via
+ # httpcore
+ # httpx
+ # requests
+cffi==1.17.1 \
+ --hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
+ --hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
+ --hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
+ --hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
+ --hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
+ --hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
+ --hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
+ --hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
+ --hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
+ --hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
+ --hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
+ --hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
+ --hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
+ --hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
+ --hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
+ --hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
+ --hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
+ --hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
+ --hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
+ --hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
+ --hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
+ --hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
+ --hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
+ --hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
+ --hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
+ --hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
+ --hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
+ --hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
+ --hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
+ --hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
+ --hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
+ --hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
+ --hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
+ --hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
+ --hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
+ --hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
+ --hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
+ --hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
+ --hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
+ --hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
+ --hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
+ --hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
+ --hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
+ --hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
+ --hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
+ --hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
+ --hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
+ # via
+ # cryptography
+ # pynacl
+charset-normalizer==3.4.2 \
+ --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
+ --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
+ --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
+ --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
+ --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
+ --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
+ --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
+ --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
+ --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
+ --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
+ --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
+ --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
+ --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
+ --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
+ --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
+ --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
+ --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
+ --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
+ --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
+ --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
+ --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
+ --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
+ --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
+ --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
+ --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
+ --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
+ --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
+ --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
+ --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
+ --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
+ --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
+ --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
+ --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
+ --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
+ --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
+ --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
+ --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
+ --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
+ --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
+ --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
+ --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
+ --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
+ --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
+ --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
+ --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
+ --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
+ --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
+ --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
+ --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
+ --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
+ --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
+ --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
+ --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
+ --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
+ # via requests
+click==8.2.1 \
+ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
+ --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
+ # via typer
+cocode==0.1.2 \
+ --hash=sha256:32aba620170da4e1855394fdf37274bf443a98fba98f0d5da1d89a06f06aa8c6 \
+ --hash=sha256:63b7f9c51bd18c55f485051d714577a5b755498a20e6a9c69fa9ed734fb47801
+ # via my-project
+colorama==0.4.6 ; sys_platform == 'win32' \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via
+ # click
+ # pytest
+ # tqdm
+cryptography==45.0.7 \
+ --hash=sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34 \
+ --hash=sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513 \
+ --hash=sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5 \
+ --hash=sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c \
+ --hash=sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63 \
+ --hash=sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130 \
+ --hash=sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae \
+ --hash=sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443 \
+ --hash=sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59 \
+ --hash=sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee \
+ --hash=sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf \
+ --hash=sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27 \
+ --hash=sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde \
+ --hash=sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971 \
+ --hash=sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8 \
+ --hash=sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339 \
+ --hash=sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6 \
+ --hash=sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90 \
+ --hash=sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691 \
+ --hash=sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3 \
+ --hash=sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083 \
+ --hash=sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6 \
+ --hash=sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1 \
+ --hash=sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3 \
+ --hash=sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8 \
+ --hash=sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2 \
+ --hash=sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7 \
+ --hash=sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141 \
+ --hash=sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3 \
+ --hash=sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9 \
+ --hash=sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4 \
+ --hash=sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4 \
+ --hash=sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b \
+ --hash=sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252 \
+ --hash=sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17 \
+ --hash=sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b \
+ --hash=sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd
+ # via pyjwt
+deprecated==1.2.18 \
+ --hash=sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d \
+ --hash=sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec
+ # via pygithub
+distro==1.9.0 \
+ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
+ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
+ # via
+ # anthropic
+ # openai
+docstring-parser==0.16 \
+ --hash=sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e \
+ --hash=sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637
+ # via instructor
+et-xmlfile==2.0.0 \
+ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
+ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
+ # via openpyxl
+eval-type-backport==0.2.2 \
+ --hash=sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a \
+ --hash=sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1
+ # via mistralai
+exceptiongroup==1.3.0 ; python_full_version < '3.11' \
+ --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \
+ --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88
+ # via
+ # anyio
+ # pytest
+faker==37.4.0 \
+ --hash=sha256:7f69d579588c23d5ce671f3fa872654ede0e67047820255f43a4aa1925b89780 \
+ --hash=sha256:cb81c09ebe06c32a10971d1bbdb264bb0e22b59af59548f011ac4809556ce533
+ # via polyfactory
+fal-client==0.7.0 \
+ --hash=sha256:9bf02cfc56ac8957159e8a959ef08c57e5618ceac2cff552f72043363b92a72f \
+ --hash=sha256:f847ff76af5bd8f4f68541eff629481730656a374e6071dbb947a2ad9212f129
+ # via pipelex
+filetype==1.2.0 \
+ --hash=sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb \
+ --hash=sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25
+ # via pipelex
+frozenlist==1.6.0 \
+ --hash=sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117 \
+ --hash=sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2 \
+ --hash=sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812 \
+ --hash=sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3 \
+ --hash=sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a \
+ --hash=sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b \
+ --hash=sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626 \
+ --hash=sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e \
+ --hash=sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878 \
+ --hash=sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e \
+ --hash=sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869 \
+ --hash=sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd \
+ --hash=sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603 \
+ --hash=sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606 \
+ --hash=sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85 \
+ --hash=sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64 \
+ --hash=sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f \
+ --hash=sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0 \
+ --hash=sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4 \
+ --hash=sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103 \
+ --hash=sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02 \
+ --hash=sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191 \
+ --hash=sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e \
+ --hash=sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff \
+ --hash=sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8 \
+ --hash=sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860 \
+ --hash=sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8 \
+ --hash=sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25 \
+ --hash=sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba \
+ --hash=sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24 \
+ --hash=sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e \
+ --hash=sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd \
+ --hash=sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911 \
+ --hash=sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c \
+ --hash=sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595 \
+ --hash=sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c \
+ --hash=sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc \
+ --hash=sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2 \
+ --hash=sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75 \
+ --hash=sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4 \
+ --hash=sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0 \
+ --hash=sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe \
+ --hash=sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249 \
+ --hash=sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c \
+ --hash=sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576 \
+ --hash=sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b \
+ --hash=sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770 \
+ --hash=sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046 \
+ --hash=sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584 \
+ --hash=sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497 \
+ --hash=sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f \
+ --hash=sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f \
+ --hash=sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f \
+ --hash=sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d \
+ --hash=sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a \
+ --hash=sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e \
+ --hash=sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68 \
+ --hash=sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9 \
+ --hash=sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8 \
+ --hash=sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1 \
+ --hash=sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0 \
+ --hash=sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29 \
+ --hash=sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0 \
+ --hash=sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215 \
+ --hash=sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590 \
+ --hash=sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c \
+ --hash=sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821 \
+ --hash=sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1 \
+ --hash=sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769 \
+ --hash=sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506 \
+ --hash=sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3 \
+ --hash=sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348 \
+ --hash=sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a \
+ --hash=sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad \
+ --hash=sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6 \
+ --hash=sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45 \
+ --hash=sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188 \
+ --hash=sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e \
+ --hash=sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70 \
+ --hash=sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1 \
+ --hash=sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106 \
+ --hash=sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd \
+ --hash=sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc \
+ --hash=sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352 \
+ --hash=sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91 \
+ --hash=sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1 \
+ --hash=sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f
+ # via
+ # aiohttp
+ # aiosignal
+google-auth==2.40.2 \
+ --hash=sha256:a33cde547a2134273226fa4b853883559947ebe9207521f7afc707efbf690f58 \
+ --hash=sha256:f7e568d42eedfded58734f6a60c58321896a621f7c116c411550a4b4a13da90b
+ # via google-auth-oauthlib
+google-auth-oauthlib==1.2.2 \
+ --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \
+ --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2
+ # via pipelex
+h11==0.16.0 \
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
+ # via httpcore
+httpcore==1.0.9 \
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
+ # via httpx
+httpx==0.28.1 \
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
+ # via
+ # anthropic
+ # fal-client
+ # mistralai
+ # openai
+ # pipelex
+httpx-sse==0.4.0 \
+ --hash=sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721 \
+ --hash=sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f
+ # via fal-client
+idna==3.10 \
+ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
+ # via
+ # anyio
+ # httpx
+ # requests
+ # yarl
+iniconfig==2.1.0 \
+ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \
+ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
+ # via pytest
+instructor==1.8.3 \
+ --hash=sha256:04d64ebc0d6e5eee104f4715b18fac1a02c21757ae6ce76efa65d38cbd536b0b \
+ --hash=sha256:a2c5066458132dc50ec6faa87cab9a2dd6b11b3f45c0e563a2ef704892286945
+ # via pipelex
+jinja2==3.1.6 \
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
+ # via
+ # instructor
+ # pipelex
+jiter==0.8.2 \
+ --hash=sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60 \
+ --hash=sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887 \
+ --hash=sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f \
+ --hash=sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b \
+ --hash=sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74 \
+ --hash=sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c \
+ --hash=sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566 \
+ --hash=sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff \
+ --hash=sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105 \
+ --hash=sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18 \
+ --hash=sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6 \
+ --hash=sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4 \
+ --hash=sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3 \
+ --hash=sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587 \
+ --hash=sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f \
+ --hash=sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1 \
+ --hash=sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44 \
+ --hash=sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43 \
+ --hash=sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef \
+ --hash=sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44 \
+ --hash=sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a \
+ --hash=sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6 \
+ --hash=sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e \
+ --hash=sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c \
+ --hash=sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586 \
+ --hash=sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88 \
+ --hash=sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d \
+ --hash=sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9 \
+ --hash=sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15 \
+ --hash=sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0 \
+ --hash=sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865 \
+ --hash=sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08 \
+ --hash=sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393 \
+ --hash=sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0 \
+ --hash=sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca \
+ --hash=sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d \
+ --hash=sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29 \
+ --hash=sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84 \
+ --hash=sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b \
+ --hash=sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49 \
+ --hash=sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d \
+ --hash=sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855 \
+ --hash=sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc \
+ --hash=sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099 \
+ --hash=sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66 \
+ --hash=sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d \
+ --hash=sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f \
+ --hash=sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152 \
+ --hash=sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05 \
+ --hash=sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57 \
+ --hash=sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5 \
+ --hash=sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e
+ # via
+ # anthropic
+ # instructor
+ # openai
+jmespath==1.0.1 \
+ --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \
+ --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe
+ # via
+ # aiobotocore
+ # boto3
+ # botocore
+json2html==1.3.0 \
+ --hash=sha256:8951a53662ae9cfd812685facdba693fc950ffc1c1fd1a8a2d3cf4c34600689c
+ # via pipelex
+jsonpath-python==1.0.6 \
+ --hash=sha256:1e3b78df579f5efc23565293612decee04214609208a2335884b3ee3f786b575 \
+ --hash=sha256:dd5be4a72d8a2995c3f583cf82bf3cd1a9544cfdabf2d22595b67aff07349666
+ # via mistralai
+kajson==0.3.0 \
+ --hash=sha256:5ea8e164cd7fc96877bf4038cf64c286ac4606814146c24dc9ecc4a2c6ac047d \
+ --hash=sha256:d8cc72875ebef6ea44c06c96fe63accf5b0721ca24ea1e593814dd45fd25bf15
+ # via pipelex
+markdown==3.8 \
+ --hash=sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc \
+ --hash=sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f
+ # via pipelex
+markdown-it-py==3.0.0 \
+ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
+ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
+ # via rich
+markupsafe==3.0.2 \
+ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
+ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
+ --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
+ --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
+ --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
+ --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
+ --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
+ --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
+ --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
+ --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
+ --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
+ --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
+ --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
+ --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
+ --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
+ --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
+ --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
+ --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
+ --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
+ --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
+ --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
+ --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
+ --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
+ --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
+ --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
+ --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
+ --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
+ --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
+ --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
+ --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
+ --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
+ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
+ --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
+ --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
+ --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
+ --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
+ --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
+ --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
+ --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
+ --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
+ --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
+ --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
+ --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
+ --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
+ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
+ --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
+ --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
+ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
+ --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
+ --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
+ --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
+ # via jinja2
+mdurl==0.1.2 \
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+ # via markdown-it-py
+mistralai==1.5.2 \
+ --hash=sha256:5b1112acebbcad1afd7732ce0bd60614975b64999801c555c54768ac41f506ae \
+ --hash=sha256:f39e6e51e8939aac2602e4badcb18712cbee2df33d86100c559333e609b92d17
+ # via pipelex
+multidict==6.4.4 \
+ --hash=sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c \
+ --hash=sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0 \
+ --hash=sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c \
+ --hash=sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd \
+ --hash=sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145 \
+ --hash=sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f \
+ --hash=sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c \
+ --hash=sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1 \
+ --hash=sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2 \
+ --hash=sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e \
+ --hash=sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f \
+ --hash=sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49 \
+ --hash=sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd \
+ --hash=sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915 \
+ --hash=sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95 \
+ --hash=sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772 \
+ --hash=sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51 \
+ --hash=sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e \
+ --hash=sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275 \
+ --hash=sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42 \
+ --hash=sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd \
+ --hash=sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601 \
+ --hash=sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44 \
+ --hash=sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781 \
+ --hash=sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b \
+ --hash=sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3 \
+ --hash=sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de \
+ --hash=sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031 \
+ --hash=sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8 \
+ --hash=sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf \
+ --hash=sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156 \
+ --hash=sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9 \
+ --hash=sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529 \
+ --hash=sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0 \
+ --hash=sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780 \
+ --hash=sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc \
+ --hash=sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9 \
+ --hash=sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f \
+ --hash=sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed \
+ --hash=sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7 \
+ --hash=sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff \
+ --hash=sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1 \
+ --hash=sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373 \
+ --hash=sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c \
+ --hash=sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a \
+ --hash=sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d \
+ --hash=sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69 \
+ --hash=sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15 \
+ --hash=sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299 \
+ --hash=sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04 \
+ --hash=sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740 \
+ --hash=sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065 \
+ --hash=sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01 \
+ --hash=sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e \
+ --hash=sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f \
+ --hash=sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26 \
+ --hash=sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1 \
+ --hash=sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a \
+ --hash=sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b \
+ --hash=sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08 \
+ --hash=sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028 \
+ --hash=sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93 \
+ --hash=sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb \
+ --hash=sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad \
+ --hash=sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223 \
+ --hash=sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20 \
+ --hash=sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac \
+ --hash=sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e \
+ --hash=sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a \
+ --hash=sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab \
+ --hash=sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4 \
+ --hash=sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0 \
+ --hash=sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd \
+ --hash=sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683 \
+ --hash=sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc \
+ --hash=sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645 \
+ --hash=sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e \
+ --hash=sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925 \
+ --hash=sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0 \
+ --hash=sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046 \
+ --hash=sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d \
+ --hash=sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598 \
+ --hash=sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2 \
+ --hash=sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2 \
+ --hash=sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2 \
+ --hash=sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b \
+ --hash=sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482
+ # via
+ # aiobotocore
+ # aiohttp
+ # yarl
+mypy==1.16.0 \
+ --hash=sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c \
+ --hash=sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff \
+ --hash=sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93 \
+ --hash=sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0 \
+ --hash=sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031 \
+ --hash=sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777 \
+ --hash=sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b \
+ --hash=sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb \
+ --hash=sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666 \
+ --hash=sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c \
+ --hash=sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2 \
+ --hash=sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab \
+ --hash=sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491 \
+ --hash=sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab \
+ --hash=sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d \
+ --hash=sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b \
+ --hash=sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52 \
+ --hash=sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8 \
+ --hash=sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec \
+ --hash=sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13 \
+ --hash=sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b \
+ --hash=sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1 \
+ --hash=sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571 \
+ --hash=sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730 \
+ --hash=sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090 \
+ --hash=sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b
+ # via my-project
+mypy-extensions==1.1.0 \
+ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
+ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
+ # via
+ # mypy
+ # typing-inspect
+networkx==3.4.2 ; python_full_version < '3.11' \
+ --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \
+ --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f
+ # via pipelex
+networkx==3.5 ; python_full_version >= '3.11' \
+ --hash=sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec \
+ --hash=sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037
+ # via pipelex
+nodeenv==1.9.1 \
+ --hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
+ --hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
+ # via pyright
+numpy==2.2.6 \
+ --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
+ --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
+ --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
+ --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
+ --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
+ --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
+ --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
+ --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
+ --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
+ --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
+ --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
+ --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
+ --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
+ --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
+ --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
+ --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
+ --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
+ --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
+ --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
+ --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
+ --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
+ --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
+ --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
+ --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
+ --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
+ --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
+ --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
+ --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
+ --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
+ --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
+ --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
+ --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
+ --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
+ --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
+ --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
+ --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
+ --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
+ --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
+ --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
+ --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
+ --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
+ --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
+ --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
+ --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
+ --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
+ --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
+ --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
+ --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
+ --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
+ --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
+ --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
+ --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
+ --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
+ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
+ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
+ # via
+ # pandas
+ # types-networkx
+oauthlib==3.2.2 \
+ --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \
+ --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918
+ # via requests-oauthlib
+openai==1.82.1 \
+ --hash=sha256:334eb5006edf59aa464c9e932b9d137468d810b2659e5daea9b3a8c39d052395 \
+ --hash=sha256:ffc529680018e0417acac85f926f92aa0bbcbc26e82e2621087303c66bc7f95d
+ # via
+ # instructor
+ # pipelex
+openpyxl==3.1.5 \
+ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \
+ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050
+ # via pipelex
+packaging==25.0 \
+ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \
+ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f
+ # via
+ # pytest
+ # pytest-sugar
+pandas==2.2.3 \
+ --hash=sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a \
+ --hash=sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d \
+ --hash=sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5 \
+ --hash=sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4 \
+ --hash=sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0 \
+ --hash=sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32 \
+ --hash=sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28 \
+ --hash=sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f \
+ --hash=sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348 \
+ --hash=sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18 \
+ --hash=sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468 \
+ --hash=sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5 \
+ --hash=sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667 \
+ --hash=sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645 \
+ --hash=sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13 \
+ --hash=sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3 \
+ --hash=sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d \
+ --hash=sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb \
+ --hash=sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3 \
+ --hash=sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039 \
+ --hash=sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8 \
+ --hash=sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd \
+ --hash=sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659 \
+ --hash=sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57 \
+ --hash=sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4 \
+ --hash=sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a \
+ --hash=sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9 \
+ --hash=sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42 \
+ --hash=sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2 \
+ --hash=sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc \
+ --hash=sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698 \
+ --hash=sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed \
+ --hash=sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015 \
+ --hash=sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24 \
+ --hash=sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319
+ # via pipelex
+pathspec==0.12.1 \
+ --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
+ --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
+ # via mypy
+pillow==11.2.1 \
+ --hash=sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b \
+ --hash=sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91 \
+ --hash=sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97 \
+ --hash=sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4 \
+ --hash=sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193 \
+ --hash=sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95 \
+ --hash=sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941 \
+ --hash=sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f \
+ --hash=sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f \
+ --hash=sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3 \
+ --hash=sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044 \
+ --hash=sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb \
+ --hash=sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681 \
+ --hash=sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d \
+ --hash=sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2 \
+ --hash=sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d \
+ --hash=sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406 \
+ --hash=sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70 \
+ --hash=sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e \
+ --hash=sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013 \
+ --hash=sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d \
+ --hash=sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2 \
+ --hash=sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7 \
+ --hash=sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751 \
+ --hash=sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c \
+ --hash=sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c \
+ --hash=sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c \
+ --hash=sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b \
+ --hash=sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd \
+ --hash=sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691 \
+ --hash=sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14 \
+ --hash=sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b \
+ --hash=sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f \
+ --hash=sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0 \
+ --hash=sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed \
+ --hash=sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0 \
+ --hash=sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22 \
+ --hash=sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788 \
+ --hash=sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16 \
+ --hash=sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156 \
+ --hash=sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad \
+ --hash=sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076 \
+ --hash=sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7 \
+ --hash=sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e \
+ --hash=sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6 \
+ --hash=sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772 \
+ --hash=sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155 \
+ --hash=sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830 \
+ --hash=sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4 \
+ --hash=sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61 \
+ --hash=sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8 \
+ --hash=sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01 \
+ --hash=sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1 \
+ --hash=sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d \
+ --hash=sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579 \
+ --hash=sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6 \
+ --hash=sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1 \
+ --hash=sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7 \
+ --hash=sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047 \
+ --hash=sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443 \
+ --hash=sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf \
+ --hash=sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd \
+ --hash=sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193 \
+ --hash=sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600 \
+ --hash=sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c \
+ --hash=sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363 \
+ --hash=sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e \
+ --hash=sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9 \
+ --hash=sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28 \
+ --hash=sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b
+ # via pipelex
+pipelex @ git+https://github.com/Pipelex/pipelex.git@9d93639f6abe0beeb485138a78ae14042537cf17
+ # via
+ # cocode
+ # my-project
+pluggy==1.6.0 \
+ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
+ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
+ # via pytest
+polyfactory==2.21.0 \
+ --hash=sha256:9483b764756c8622313d99f375889b1c0d92f09affb05742d7bcfa2b5198d8c5 \
+ --hash=sha256:a6d8dba91b2515d744cc014b5be48835633f7ccb72519a68f8801759e5b1737a
+ # via pipelex
+propcache==0.3.1 \
+ --hash=sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e \
+ --hash=sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b \
+ --hash=sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b \
+ --hash=sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c \
+ --hash=sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a \
+ --hash=sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf \
+ --hash=sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8 \
+ --hash=sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5 \
+ --hash=sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42 \
+ --hash=sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0 \
+ --hash=sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e \
+ --hash=sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46 \
+ --hash=sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d \
+ --hash=sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24 \
+ --hash=sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d \
+ --hash=sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de \
+ --hash=sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf \
+ --hash=sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7 \
+ --hash=sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371 \
+ --hash=sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833 \
+ --hash=sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259 \
+ --hash=sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136 \
+ --hash=sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25 \
+ --hash=sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005 \
+ --hash=sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef \
+ --hash=sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7 \
+ --hash=sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f \
+ --hash=sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53 \
+ --hash=sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0 \
+ --hash=sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb \
+ --hash=sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a \
+ --hash=sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458 \
+ --hash=sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9 \
+ --hash=sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71 \
+ --hash=sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b \
+ --hash=sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5 \
+ --hash=sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037 \
+ --hash=sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757 \
+ --hash=sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3 \
+ --hash=sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976 \
+ --hash=sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6 \
+ --hash=sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7 \
+ --hash=sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649 \
+ --hash=sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120 \
+ --hash=sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd \
+ --hash=sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40 \
+ --hash=sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e \
+ --hash=sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229 \
+ --hash=sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c \
+ --hash=sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7 \
+ --hash=sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111 \
+ --hash=sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654 \
+ --hash=sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f \
+ --hash=sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da \
+ --hash=sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f \
+ --hash=sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7 \
+ --hash=sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0 \
+ --hash=sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073 \
+ --hash=sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11 \
+ --hash=sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f \
+ --hash=sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27 \
+ --hash=sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70 \
+ --hash=sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7 \
+ --hash=sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519 \
+ --hash=sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5 \
+ --hash=sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180 \
+ --hash=sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f \
+ --hash=sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee \
+ --hash=sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18 \
+ --hash=sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815 \
+ --hash=sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e \
+ --hash=sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a \
+ --hash=sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7 \
+ --hash=sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc \
+ --hash=sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8 \
+ --hash=sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98 \
+ --hash=sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256 \
+ --hash=sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5 \
+ --hash=sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744 \
+ --hash=sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723 \
+ --hash=sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277 \
+ --hash=sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5
+ # via
+ # aiohttp
+ # yarl
+pyasn1==0.6.1 \
+ --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
+ --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
+ # via
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.2 \
+ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
+ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
+ # via google-auth
+pycparser==2.22 \
+ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
+ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
+ # via cffi
+pydantic==2.10.6 \
+ --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \
+ --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236
+ # via
+ # anthropic
+ # instructor
+ # kajson
+ # mistralai
+ # openai
+ # pipelex
+pydantic-core==2.27.2 \
+ --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \
+ --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \
+ --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \
+ --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \
+ --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \
+ --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \
+ --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \
+ --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \
+ --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \
+ --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \
+ --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \
+ --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \
+ --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \
+ --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \
+ --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \
+ --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \
+ --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \
+ --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \
+ --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \
+ --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \
+ --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \
+ --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \
+ --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \
+ --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \
+ --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \
+ --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \
+ --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \
+ --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \
+ --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \
+ --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \
+ --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \
+ --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \
+ --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \
+ --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \
+ --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \
+ --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \
+ --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \
+ --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \
+ --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \
+ --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \
+ --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \
+ --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \
+ --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \
+ --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \
+ --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \
+ --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \
+ --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \
+ --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \
+ --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \
+ --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \
+ --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \
+ --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \
+ --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \
+ --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \
+ --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \
+ --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \
+ --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \
+ --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \
+ --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \
+ --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \
+ --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \
+ --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \
+ --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \
+ --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \
+ --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9
+ # via
+ # instructor
+ # pydantic
+pygithub==2.4.0 \
+ --hash=sha256:6601e22627e87bac192f1e2e39c6e6f69a43152cfb8f307cee575879320b3051 \
+ --hash=sha256:81935aa4bdc939fba98fee1cb47422c09157c56a27966476ff92775602b9ee24
+ # via cocode
+pygments==2.19.1 \
+ --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \
+ --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c
+ # via rich
+pyjwt==2.10.1 \
+ --hash=sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953 \
+ --hash=sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb
+ # via pygithub
+pynacl==1.5.0 \
+ --hash=sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858 \
+ --hash=sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d \
+ --hash=sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93 \
+ --hash=sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1 \
+ --hash=sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92 \
+ --hash=sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff \
+ --hash=sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba \
+ --hash=sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394 \
+ --hash=sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b \
+ --hash=sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543
+ # via pygithub
+pypdfium2==4.30.0 \
+ --hash=sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e \
+ --hash=sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29 \
+ --hash=sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2 \
+ --hash=sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16 \
+ --hash=sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de \
+ --hash=sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854 \
+ --hash=sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163 \
+ --hash=sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c \
+ --hash=sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab \
+ --hash=sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad \
+ --hash=sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e \
+ --hash=sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f \
+ --hash=sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be
+ # via pipelex
+pyright==1.1.398 \
+ --hash=sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753 \
+ --hash=sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8
+ # via my-project
+pytest==8.3.5 \
+ --hash=sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820 \
+ --hash=sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845
+ # via
+ # my-project
+ # pytest-asyncio
+ # pytest-sugar
+pytest-asyncio==1.0.0 \
+ --hash=sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3 \
+ --hash=sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f
+ # via my-project
+pytest-sugar==1.0.0 \
+ --hash=sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a \
+ --hash=sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd
+ # via my-project
+python-dateutil==2.9.0.post0 \
+ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
+ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
+ # via
+ # aiobotocore
+ # botocore
+ # mistralai
+ # pandas
+python-dotenv==1.1.0 \
+ --hash=sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5 \
+ --hash=sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d
+ # via pipelex
+pytz==2025.2 \
+ --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
+ --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
+ # via pandas
+pyyaml==6.0.2 \
+ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
+ --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
+ --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
+ --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
+ --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
+ --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
+ --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
+ --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
+ --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
+ --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
+ --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
+ --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
+ --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
+ --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
+ --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
+ --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
+ --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
+ --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
+ --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
+ --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
+ --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
+ --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
+ --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
+ --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
+ --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
+ --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
+ --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
+ --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
+ --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
+ --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
+ --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
+ --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
+ --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
+ --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
+ --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
+ --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
+ --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
+ # via pipelex
+requests==2.32.3 \
+ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
+ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
+ # via
+ # instructor
+ # pygithub
+ # requests-oauthlib
+requests-oauthlib==2.0.0 \
+ --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \
+ --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9
+ # via google-auth-oauthlib
+rich==13.9.4 \
+ --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \
+ --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90
+ # via
+ # instructor
+ # pipelex
+ # typer
+rsa==4.9.1 \
+ --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \
+ --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75
+ # via google-auth
+ruff==0.11.12 \
+ --hash=sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd \
+ --hash=sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02 \
+ --hash=sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3 \
+ --hash=sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012 \
+ --hash=sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603 \
+ --hash=sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13 \
+ --hash=sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832 \
+ --hash=sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5 \
+ --hash=sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be \
+ --hash=sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7 \
+ --hash=sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5 \
+ --hash=sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef \
+ --hash=sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa \
+ --hash=sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a \
+ --hash=sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc \
+ --hash=sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c \
+ --hash=sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6 \
+ --hash=sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a
+ # via my-project
+s3transfer==0.11.3 \
+ --hash=sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d \
+ --hash=sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a
+ # via boto3
+shellingham==1.5.4 \
+ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
+ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
+ # via typer
+shortuuid==1.0.13 \
+ --hash=sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72 \
+ --hash=sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a
+ # via pipelex
+six==1.17.0 \
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+ # via python-dateutil
+sniffio==1.3.1 \
+ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
+ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
+ # via
+ # anthropic
+ # anyio
+ # openai
+tenacity==9.1.2 \
+ --hash=sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb \
+ --hash=sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138
+ # via instructor
+termcolor==3.1.0 \
+ --hash=sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa \
+ --hash=sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970
+ # via pytest-sugar
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via pipelex
+tomli==2.2.1 ; python_full_version < '3.11' \
+ --hash=sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6 \
+ --hash=sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd \
+ --hash=sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c \
+ --hash=sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b \
+ --hash=sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8 \
+ --hash=sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6 \
+ --hash=sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77 \
+ --hash=sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff \
+ --hash=sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea \
+ --hash=sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192 \
+ --hash=sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249 \
+ --hash=sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee \
+ --hash=sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4 \
+ --hash=sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98 \
+ --hash=sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8 \
+ --hash=sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4 \
+ --hash=sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281 \
+ --hash=sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744 \
+ --hash=sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69 \
+ --hash=sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13 \
+ --hash=sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140 \
+ --hash=sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e \
+ --hash=sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e \
+ --hash=sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc \
+ --hash=sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff \
+ --hash=sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec \
+ --hash=sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2 \
+ --hash=sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222 \
+ --hash=sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106 \
+ --hash=sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272 \
+ --hash=sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a \
+ --hash=sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7
+ # via
+ # mypy
+ # pytest
+tomlkit==0.13.3 \
+ --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
+ --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
+ # via pipelex
+tqdm==4.67.1 \
+ --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \
+ --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2
+ # via openai
+typer==0.16.0 \
+ --hash=sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855 \
+ --hash=sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b
+ # via
+ # instructor
+ # pipelex
+types-aioboto3==14.3.0 \
+ --hash=sha256:017a9916e9ffdb48b5961d3f32d7a67cebb8c46dad53c778d2bca0fc47ac9087 \
+ --hash=sha256:ba722ed63c54427e99bb3439c212576caecb42fa63aac593b6f63d60d42de170
+ # via my-project
+types-aiobotocore==2.22.0 \
+ --hash=sha256:04c6acd984f9a37d22db2ebf670ff84e55f6f227d3cc71e9b297720d4a71b718 \
+ --hash=sha256:54ffcf8d0144d71fa8b540f3997ae1a555ff3a61200d0c13b3db1c29dec730e5
+ # via types-aioboto3
+types-aiobotocore-bedrock==2.22.0 \
+ --hash=sha256:301a591b4deda3e3a04c5d73f6056e8043ee419b8c5327c6bdb2dba42ebc8022 \
+ --hash=sha256:e2106f935deb02b9c8b593bdf8ece93f7bb680906fa903aafe1f276720c4367d
+ # via types-aioboto3
+types-aiobotocore-bedrock-runtime==2.22.0 \
+ --hash=sha256:19bb3c06a62fb20980983598990f7670548b028b0c7bc429d113d7595fbab185 \
+ --hash=sha256:9c211fc08818dbd9b0261a37084291af643d1e19460aad0f12833d2f5a3b1b86
+ # via types-aioboto3
+types-aiofiles==24.1.0.20250516 \
+ --hash=sha256:7fd2a7f793bbe180b7b22cd4f59300fe61fdc9940b3bbc9899ffe32849b95188 \
+ --hash=sha256:ec265994629146804b656a971c46f393ce860305834b3cacb4b8b6fb7dba7e33
+ # via my-project
+types-awscrt==0.27.2 \
+ --hash=sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e \
+ --hash=sha256:acd04f57119eb15626ab0ba9157fc24672421de56e7bd7b9f61681fedee44e91
+ # via botocore-stubs
+types-beautifulsoup4==4.12.0.20250516 \
+ --hash=sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee \
+ --hash=sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e
+ # via my-project
+types-html5lib==1.1.11.20250516 \
+ --hash=sha256:5e407b14b1bd2b9b1107cbd1e2e19d4a0c46d60febd231c7ab7313d7405663c1 \
+ --hash=sha256:65043a6718c97f7d52567cc0cdf41efbfc33b1f92c6c0c5e19f60a7ec69ae720
+ # via types-beautifulsoup4
+types-markdown==3.8.0.20250415 \
+ --hash=sha256:98ab13587d1177769d93e55586d3dc97047df75bc6e37ce4074666f5dd4212ba \
+ --hash=sha256:b41abed474a303ba300e3a4cf6f27eda339219124a59d529a158203570007776
+ # via my-project
+types-networkx==3.5.0.20250531 \
+ --hash=sha256:84a3163c802a430a90003444eb37a7a2e59206125e5a308f5ccb00434333d2f4 \
+ --hash=sha256:e8e9adfd0cdb7255e822d029163d1ef651e12cae4a3c767198b8f3e899118ca7
+ # via my-project
+types-openpyxl==3.1.5.20250516 \
+ --hash=sha256:691339abe141a5713f115558cc39023ebdda6298bfa875d575cc5a961a3c5523 \
+ --hash=sha256:87c6b04b30fd1cbab85dc93cbe4f57ce1eb3df5d2911f742a1a0e3bf94314dfc
+ # via my-project
+types-pyyaml==6.0.12.20250516 \
+ --hash=sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530 \
+ --hash=sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba
+ # via my-project
+types-requests==2.32.0.20250515 \
+ --hash=sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581 \
+ --hash=sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2
+ # via my-project
+types-s3transfer==0.13.0 \
+ --hash=sha256:203dadcb9865c2f68fb44bc0440e1dc05b79197ba4a641c0976c26c9af75ef52 \
+ --hash=sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3
+ # via
+ # boto3-stubs
+ # types-aioboto3
+types-toml==0.10.8.20240310 \
+ --hash=sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331 \
+ --hash=sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d
+ # via my-project
+typing-extensions==4.13.2 \
+ --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
+ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
+ # via
+ # anthropic
+ # anyio
+ # boto3-stubs
+ # exceptiongroup
+ # multidict
+ # mypy
+ # openai
+ # pipelex
+ # polyfactory
+ # pydantic
+ # pydantic-core
+ # pygithub
+ # pyright
+ # rich
+ # typer
+ # types-aioboto3
+ # types-aiobotocore
+ # types-aiobotocore-bedrock
+ # types-aiobotocore-bedrock-runtime
+ # typing-inspect
+typing-inspect==0.9.0 \
+ --hash=sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f \
+ --hash=sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78
+ # via mistralai
+tzdata==2025.2 \
+ --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
+ --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
+ # via
+ # faker
+ # pandas
+urllib3==2.4.0 \
+ --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \
+ --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813
+ # via
+ # botocore
+ # pygithub
+ # requests
+ # types-requests
+wrapt==1.17.2 \
+ --hash=sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c \
+ --hash=sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a \
+ --hash=sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b \
+ --hash=sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555 \
+ --hash=sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c \
+ --hash=sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b \
+ --hash=sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6 \
+ --hash=sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662 \
+ --hash=sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998 \
+ --hash=sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62 \
+ --hash=sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984 \
+ --hash=sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392 \
+ --hash=sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2 \
+ --hash=sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306 \
+ --hash=sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3 \
+ --hash=sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9 \
+ --hash=sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6 \
+ --hash=sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192 \
+ --hash=sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317 \
+ --hash=sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f \
+ --hash=sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda \
+ --hash=sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563 \
+ --hash=sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f \
+ --hash=sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d \
+ --hash=sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8 \
+ --hash=sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845 \
+ --hash=sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82 \
+ --hash=sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125 \
+ --hash=sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504 \
+ --hash=sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b \
+ --hash=sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7 \
+ --hash=sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc \
+ --hash=sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6 \
+ --hash=sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40 \
+ --hash=sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72 \
+ --hash=sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681 \
+ --hash=sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438 \
+ --hash=sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae \
+ --hash=sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2 \
+ --hash=sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb \
+ --hash=sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5 \
+ --hash=sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a \
+ --hash=sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3 \
+ --hash=sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8 \
+ --hash=sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22 \
+ --hash=sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72 \
+ --hash=sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061 \
+ --hash=sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98 \
+ --hash=sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b \
+ --hash=sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925 \
+ --hash=sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6 \
+ --hash=sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0 \
+ --hash=sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9 \
+ --hash=sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c \
+ --hash=sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991 \
+ --hash=sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000 \
+ --hash=sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58
+ # via
+ # aiobotocore
+ # deprecated
+yarl==1.20.0 \
+ --hash=sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9 \
+ --hash=sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61 \
+ --hash=sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2 \
+ --hash=sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2 \
+ --hash=sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33 \
+ --hash=sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902 \
+ --hash=sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2 \
+ --hash=sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0 \
+ --hash=sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0 \
+ --hash=sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569 \
+ --hash=sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f \
+ --hash=sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7 \
+ --hash=sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00 \
+ --hash=sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1 \
+ --hash=sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f \
+ --hash=sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd \
+ --hash=sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c \
+ --hash=sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f \
+ --hash=sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5 \
+ --hash=sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d \
+ --hash=sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501 \
+ --hash=sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3 \
+ --hash=sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2 \
+ --hash=sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c \
+ --hash=sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae \
+ --hash=sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de \
+ --hash=sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a \
+ --hash=sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe \
+ --hash=sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8 \
+ --hash=sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124 \
+ --hash=sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb \
+ --hash=sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc \
+ --hash=sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2 \
+ --hash=sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac \
+ --hash=sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307 \
+ --hash=sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58 \
+ --hash=sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e \
+ --hash=sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e \
+ --hash=sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62 \
+ --hash=sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b \
+ --hash=sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe \
+ --hash=sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda \
+ --hash=sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5 \
+ --hash=sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64 \
+ --hash=sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229 \
+ --hash=sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62 \
+ --hash=sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6 \
+ --hash=sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d \
+ --hash=sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791 \
+ --hash=sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672 \
+ --hash=sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb \
+ --hash=sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a \
+ --hash=sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10 \
+ --hash=sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e \
+ --hash=sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9 \
+ --hash=sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5 \
+ --hash=sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da \
+ --hash=sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a \
+ --hash=sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d \
+ --hash=sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195 \
+ --hash=sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594 \
+ --hash=sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8 \
+ --hash=sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634 \
+ --hash=sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051 \
+ --hash=sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e \
+ --hash=sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384 \
+ --hash=sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076 \
+ --hash=sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018 \
+ --hash=sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19 \
+ --hash=sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a \
+ --hash=sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4 \
+ --hash=sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2 \
+ --hash=sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145 \
+ --hash=sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995 \
+ --hash=sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6 \
+ --hash=sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f \
+ --hash=sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f \
+ --hash=sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487 \
+ --hash=sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9 \
+ --hash=sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a \
+ --hash=sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f \
+ --hash=sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1 \
+ --hash=sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22 \
+ --hash=sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d \
+ --hash=sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877 \
+ --hash=sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867 \
+ --hash=sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3
+ # via aiohttp
+yattag==1.16.1 \
+ --hash=sha256:baa8f254e7ea5d3e0618281ad2ff5610e0e5360b3608e695c29bfb3b29d051f4
+ # via pipelex
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0cdedbb
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,1322 @@
+# This file was autogenerated by uv via the following command:
+# uv export --no-dev --output-file requirements.txt
+aioboto3==14.3.0 \
+ --hash=sha256:1d18f88bb56835c607b62bb6cb907754d717bedde3ddfff6935727cb48a80135 \
+ --hash=sha256:aec5de94e9edc1ffbdd58eead38a37f00ddac59a519db749a910c20b7b81bca7
+ # via pipelex
+aiobotocore==2.22.0 \
+ --hash=sha256:11091477266b75c2b5d28421c1f2bc9a87d175d0b8619cb830805e7a113a170b \
+ --hash=sha256:b4e6306f79df9d81daff1f9d63189a2dbee4b77ce3ab937304834e35eaaeeccf
+ # via aioboto3
+aiofiles==23.2.1 \
+ --hash=sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107 \
+ --hash=sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a
+ # via
+ # aioboto3
+ # pipelex
+aiohappyeyeballs==2.6.1 \
+ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \
+ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8
+ # via aiohttp
+aiohttp==3.12.6 \
+ --hash=sha256:012ea107092d4465aeeb681d5b2fb8b51a847a72f0b71906f40876419fba1355 \
+ --hash=sha256:06f20adcdc4f383aeb7ce884705faea44c0376cde5cdee4d32ef62d6cb1f97cc \
+ --hash=sha256:128603479bf13479661d763e77e254139f066914227b5f2ff3284d19e416ad75 \
+ --hash=sha256:148ffa6b2b825ff8520844ce23df9e2a5b969bb6917c4e35a832fbaa025d260d \
+ --hash=sha256:15817882d25e840aba85d1f5706a7128350b81050f8ca9dabfc25a5f521a792c \
+ --hash=sha256:24d19cbd1d21d207ee855500d2033f1852b4d2113a741246ff62eb16a3921306 \
+ --hash=sha256:259269870d9783de87c0430760b2498b770201ead3e11ee86761d268ce5d196a \
+ --hash=sha256:25dac87ee297e2b5826ce8e96c7615ebe7a1613856b1614a207e3376b776021b \
+ --hash=sha256:25de52753386b0c16d5acd2153e7819f52c9e7fc05f5eca804adc174e99b735d \
+ --hash=sha256:2a74a566872f41247774980334e5b0309dac11b402e188bde6db8a57de4506cd \
+ --hash=sha256:2e4fb0d7f221c36ed8469c1d2d9a2bb6a27b543cf90aa46ca701f63fb83dd7ed \
+ --hash=sha256:30511c5e66ac4399d46b4bec57a3d56bc16cfb649255fa798ee95d8b45f97a4b \
+ --hash=sha256:3331ef09dd775302aa5f4d3170bd46659ad018843fab3656f5e72e3ff68df21f \
+ --hash=sha256:37b1c6034a1e14764adad1829cd710543b1699d7985e1d336f0aa52a2dd76ba9 \
+ --hash=sha256:38af291559401d13eb90259ba79ef6ac537ae6b5bdb1251604606a88cd0fd5e0 \
+ --hash=sha256:3cc06a99e065ed7e766d2cd574671428261c1b8f30fedfbd91ab3c738fd9c08d \
+ --hash=sha256:52ce7e90ee9dd25bcd2ed4513e650cc4f9a03bef07a39193b82fb58892004bd6 \
+ --hash=sha256:545f89c389a47bac024655b5676658f35f80b0d007e4c3c7ff865d9aa3bf343a \
+ --hash=sha256:561f545dc062e6c31fc53535d8584c06516bda2fc37821a67a61b69202061e71 \
+ --hash=sha256:58f79b376a426961418df1d08656ec3a01494b7ba81824ae629e6636deddfff7 \
+ --hash=sha256:59e19517abef2af49cff79b8a863497036ff401051c79d6a3b6149a48213a7be \
+ --hash=sha256:5b700cf48fd04b4328965d1afe01f835fe6cdecc3b85ca2d950431e5cc0647f7 \
+ --hash=sha256:5fe1d74ab6cd1f16c3c2f0e3c3230481dcedc0d3ad9f0b82b1e43f44a4980aca \
+ --hash=sha256:6860351cfba0196db2edc387cfeddaf1dae443e55f261ea2bcb77fecb33aae34 \
+ --hash=sha256:6ca81cb1e41d251cc193164409c0bbb0175e696a9997491a10db9171a2f70603 \
+ --hash=sha256:71905d34b3bb1a6be44e986f08404987bb317d890746e71f320cd10cf3222b46 \
+ --hash=sha256:7487f707a4b8167394f6afefa690198300d8a618505583eb536b92202bdec24d \
+ --hash=sha256:77ba53286c89486e8b02fb47352a5a8270bab1084e2a43fe8e35eb261befda13 \
+ --hash=sha256:7d162c4f87f9dcdc7151f6329438de96beb527820381e3159ce08544c57e9ced \
+ --hash=sha256:7f22a0d9a995c12bb20247334b414edaf65ce8f22a1e838b90210238f9b57571 \
+ --hash=sha256:86fb0a5762f936606dcab1ca248f5053587a598ed44825f4744ce3c53ae9a2e9 \
+ --hash=sha256:8885da8ae99bbe6ce43b79e284ef8e6bc5285dea297fe2a163552f09435c8069 \
+ --hash=sha256:8a88046a5adddf5d99f15a1920f6b8f659f46a4cfb5bfabbd668d06df045df7a \
+ --hash=sha256:8ea77675818fd8cac28491d0d59582e5e2e5b14dbf5e21bef797aa5b23b5ca8b \
+ --hash=sha256:938afd243c9ee76a6d78fad10ecca14b88b48b71553e0e9c74b8098efff5ddf8 \
+ --hash=sha256:93a0887cea23f76e9354235b0e79b3c9922ad66529e11637940b6439849105cb \
+ --hash=sha256:93f207a64989346bbd0a9d3b31ebaa3934ea6e0242b555491af7eb97ad1c0a5a \
+ --hash=sha256:9aecb4ce110c9d321860a00b4f9ec72bef691d045f54c983fa678606f3f918b0 \
+ --hash=sha256:9dd9211229fa2f474da01d42fafff196f607a63aaf12d8b34928c43a713eb6d5 \
+ --hash=sha256:a057680218430231eb6ab644d166b7ef398b3ffbac0232f4f789cdce9391400e \
+ --hash=sha256:a1532ea3f41a818d4f50db96306a1975bf31f29787802bec4c63c58f61b6e682 \
+ --hash=sha256:a2f3c974874bd0c76dfdcc60db5a6f96ca023a85318a5ac401603baa7e299272 \
+ --hash=sha256:a52aa39eb1160775a6e80e3025c990e8872c8927c5dd4b51304788bc149b9549 \
+ --hash=sha256:a90b6f2d5ca4d3ad56034863237b59b4a5fab270eb6d11b5c0326b4501448b51 \
+ --hash=sha256:aac87d78f55057ab48ddcc43055620546d40bbc0888d2658d8705d183c98f901 \
+ --hash=sha256:b2e026a9f9ac0df70f14ca5dcaf1f83a55b678e51aa6515d710dd879d2691fd7 \
+ --hash=sha256:bc4be1d8d68a62859f74f9ada9e174791895366601ce66342f54478d3518c8b3 \
+ --hash=sha256:c05776d1854ae9d8132d7ced7ac0067f602d66589797788ed3902d5c68686db5 \
+ --hash=sha256:c1d8a4a5a7e28d8b9ec815ffecca8712b71130a4eee1c5b45e9f2cc4975f3f7c \
+ --hash=sha256:c232720190ca4240c15abefc7b765e987ef88df44d2384612890db87b33898f3 \
+ --hash=sha256:c88ed8c54f7fd6102ef711d24710454707cde4bb3ffdec09982dcb3cb966a3e1 \
+ --hash=sha256:cdb03da5ecf74a331511604f3cf91563bf29127eabb28f4e16d390a73cb826da \
+ --hash=sha256:ce6673b73352edb17c2db86a9586dc7744e0b5009709152a1e75379f16af19e0 \
+ --hash=sha256:cfbf8ed94b57e3b5a886bfe2a530c8eb067064cc4419fd94431a2cbeeddec54c \
+ --hash=sha256:d557918fefb29884335e1a257df6c961f35ba1caf8eddaabad762b3436cf87ff \
+ --hash=sha256:d590b36c3497ecfba4aca71ab9342fb2c07e1b69baf4e28ad4227440c128bb22 \
+ --hash=sha256:d5f698e7b5b57aa4dc646c8f13ccd965c694199595d7a45cecefaf0e5c392890 \
+ --hash=sha256:d7ff55a38fc9851fa5cff41b30605534dfe4d57d02f79447abfed01499fe31d3 \
+ --hash=sha256:d83ab494eb583ba691af9d4d7c073987526bb9f73aa5a19907258ef3a1e39e8a \
+ --hash=sha256:da073f88270aa434ef16a78c21a4269c96c68badc2b9ad5011fa175c06143eee \
+ --hash=sha256:db5c402ea0aed10af2e54e5946bf32f3ebb02a7604eaaa4c41a608053889de4a \
+ --hash=sha256:de83f567e31418fd7bc22c5a03526a2b0a82e68c7a7fec23ef91a398228f559b \
+ --hash=sha256:deddf6b1c83ce518a156b7597a0d7a1a7ec5c1d2c973ba3f1a23f18fa2b7d65e \
+ --hash=sha256:e5c6869319c0a5f4150959e065c40836b18a99e02493c3b4c73b25378aa0f0cc \
+ --hash=sha256:e8da054804352e974f4349fb871b07c8ffa1978e64cfb455e88fbe6fbe4d6dcb \
+ --hash=sha256:ed4db015494a6d0acaadce035531f9fb321afab2075a4b348811e4f7795e87e6 \
+ --hash=sha256:eefd98dd043c33c45123c56a79c6c39acb628304337c90f16f33569cc3aa4ba6 \
+ --hash=sha256:efbbde2297e4ab10d187103aba9b565277c85ac7d24d98cae201c033ce885504 \
+ --hash=sha256:fd1d6116c1364ab00ffed1654a01091dc7f897d315c5103bcc6e5ab7f70172c7
+ # via
+ # aiobotocore
+ # instructor
+aioitertools==0.12.0 \
+ --hash=sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b \
+ --hash=sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796
+ # via aiobotocore
+aiosignal==1.3.2 \
+ --hash=sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5 \
+ --hash=sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54
+ # via aiohttp
+annotated-types==0.7.0 \
+ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
+ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
+ # via pydantic
+anthropic==0.52.1 \
+ --hash=sha256:807cee7ebc5503753da0403a77932decf5a4c036041ddda58b4edcdb2a3da551 \
+ --hash=sha256:da4a7c3aeac0170cb45a42dc3369ca1fcf2b3238edf845cb056505d4b0c42fcf
+ # via pipelex
+anyio==4.9.0 \
+ --hash=sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028 \
+ --hash=sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c
+ # via
+ # anthropic
+ # httpx
+ # openai
+async-timeout==5.0.1 ; python_full_version < '3.11' \
+ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \
+ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3
+ # via aiohttp
+attrs==25.3.0 \
+ --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \
+ --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b
+ # via aiohttp
+backports-strenum==1.3.1 ; python_full_version < '3.11' \
+ --hash=sha256:77c52407342898497714f0596e86188bb7084f89063226f4ba66863482f42414 \
+ --hash=sha256:cdcfe36dc897e2615dc793b7d3097f54d359918fc448754a517e6f23044ccf83
+ # via pipelex
+boto3==1.37.3 \
+ --hash=sha256:2063b40af99fd02f6228ff52397b552ff3353831edaf8d25cc04801827ab9794 \
+ --hash=sha256:21f3ce0ef111297e63a6eb998a25197b8c10982970c320d4c6e8db08be2157be
+ # via
+ # aiobotocore
+ # pipelex
+botocore==1.37.3 \
+ --hash=sha256:d01bd3bf4c80e61fa88d636ad9f5c9f60a551d71549b481386c6b4efe0bb2b2e \
+ --hash=sha256:fe8403eb55a88faf9b0f9da6615e5bee7be056d75e17af66c3c8f0a3b0648da4
+ # via
+ # aiobotocore
+ # boto3
+ # s3transfer
+cachetools==5.5.2 \
+ --hash=sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4 \
+ --hash=sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a
+ # via google-auth
+certifi==2025.4.26 \
+ --hash=sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6 \
+ --hash=sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3
+ # via
+ # httpcore
+ # httpx
+ # requests
+charset-normalizer==3.4.2 \
+ --hash=sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7 \
+ --hash=sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0 \
+ --hash=sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7 \
+ --hash=sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d \
+ --hash=sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0 \
+ --hash=sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db \
+ --hash=sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b \
+ --hash=sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8 \
+ --hash=sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff \
+ --hash=sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e \
+ --hash=sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148 \
+ --hash=sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a \
+ --hash=sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e \
+ --hash=sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63 \
+ --hash=sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c \
+ --hash=sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366 \
+ --hash=sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5 \
+ --hash=sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c \
+ --hash=sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b \
+ --hash=sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0 \
+ --hash=sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941 \
+ --hash=sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0 \
+ --hash=sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86 \
+ --hash=sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6 \
+ --hash=sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0 \
+ --hash=sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1 \
+ --hash=sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6 \
+ --hash=sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981 \
+ --hash=sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c \
+ --hash=sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980 \
+ --hash=sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645 \
+ --hash=sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7 \
+ --hash=sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd \
+ --hash=sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef \
+ --hash=sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2 \
+ --hash=sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d \
+ --hash=sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3 \
+ --hash=sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd \
+ --hash=sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214 \
+ --hash=sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd \
+ --hash=sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a \
+ --hash=sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c \
+ --hash=sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f \
+ --hash=sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28 \
+ --hash=sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691 \
+ --hash=sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82 \
+ --hash=sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a \
+ --hash=sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf \
+ --hash=sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b \
+ --hash=sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9 \
+ --hash=sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544 \
+ --hash=sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509 \
+ --hash=sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a \
+ --hash=sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f
+ # via requests
+click==8.2.1 \
+ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \
+ --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b
+ # via typer
+colorama==0.4.6 ; sys_platform == 'win32' \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via
+ # click
+ # tqdm
+distro==1.9.0 \
+ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
+ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
+ # via
+ # anthropic
+ # openai
+docstring-parser==0.16 \
+ --hash=sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e \
+ --hash=sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637
+ # via instructor
+et-xmlfile==2.0.0 \
+ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
+ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54
+ # via openpyxl
+eval-type-backport==0.2.2 \
+ --hash=sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a \
+ --hash=sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1
+ # via mistralai
+exceptiongroup==1.3.0 ; python_full_version < '3.11' \
+ --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \
+ --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88
+ # via anyio
+faker==37.4.0 \
+ --hash=sha256:7f69d579588c23d5ce671f3fa872654ede0e67047820255f43a4aa1925b89780 \
+ --hash=sha256:cb81c09ebe06c32a10971d1bbdb264bb0e22b59af59548f011ac4809556ce533
+ # via polyfactory
+fal-client==0.7.0 \
+ --hash=sha256:9bf02cfc56ac8957159e8a959ef08c57e5618ceac2cff552f72043363b92a72f \
+ --hash=sha256:f847ff76af5bd8f4f68541eff629481730656a374e6071dbb947a2ad9212f129
+ # via pipelex
+filetype==1.2.0 \
+ --hash=sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb \
+ --hash=sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25
+ # via pipelex
+frozenlist==1.6.0 \
+ --hash=sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117 \
+ --hash=sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2 \
+ --hash=sha256:0e6f8653acb82e15e5443dba415fb62a8732b68fe09936bb6d388c725b57f812 \
+ --hash=sha256:0f2ca7810b809ed0f1917293050163c7654cefc57a49f337d5cd9de717b8fad3 \
+ --hash=sha256:118e97556306402e2b010da1ef21ea70cb6d6122e580da64c056b96f524fbd6a \
+ --hash=sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b \
+ --hash=sha256:1c6eceb88aaf7221f75be6ab498dc622a151f5f88d536661af3ffc486245a626 \
+ --hash=sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e \
+ --hash=sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878 \
+ --hash=sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e \
+ --hash=sha256:36d2fc099229f1e4237f563b2a3e0ff7ccebc3999f729067ce4e64a97a7f2869 \
+ --hash=sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd \
+ --hash=sha256:3e911391bffdb806001002c1f860787542f45916c3baf764264a52765d5a5603 \
+ --hash=sha256:431ef6937ae0f853143e2ca67d6da76c083e8b1fe3df0e96f3802fd37626e606 \
+ --hash=sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85 \
+ --hash=sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64 \
+ --hash=sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f \
+ --hash=sha256:49ba23817781e22fcbd45fd9ff2b9b8cdb7b16a42a4851ab8025cae7b22e96d0 \
+ --hash=sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4 \
+ --hash=sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103 \
+ --hash=sha256:52021b528f1571f98a7d4258c58aa8d4b1a96d4f01d00d51f1089f2e0323cb02 \
+ --hash=sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191 \
+ --hash=sha256:54dece0d21dce4fdb188a1ffc555926adf1d1c516e493c2914d7c370e454bc9e \
+ --hash=sha256:62c828a5b195570eb4b37369fcbbd58e96c905768d53a44d13044355647838ff \
+ --hash=sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8 \
+ --hash=sha256:654e4ba1d0b2154ca2f096bed27461cf6160bc7f504a7f9a9ef447c293caf860 \
+ --hash=sha256:69bbd454f0fb23b51cadc9bdba616c9678e4114b6f9fa372d462ff2ed9323ec8 \
+ --hash=sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25 \
+ --hash=sha256:716bbba09611b4663ecbb7cd022f640759af8259e12a6ca939c0a6acd49eedba \
+ --hash=sha256:75ecee69073312951244f11b8627e3700ec2bfe07ed24e3a685a5979f0412d24 \
+ --hash=sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e \
+ --hash=sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd \
+ --hash=sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911 \
+ --hash=sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c \
+ --hash=sha256:7b8c4dc422c1a3ffc550b465090e53b0bf4839047f3e436a34172ac67c45d595 \
+ --hash=sha256:7daa508e75613809c7a57136dec4871a21bca3080b3a8fc347c50b187df4f00c \
+ --hash=sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc \
+ --hash=sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2 \
+ --hash=sha256:89ffdb799154fd4d7b85c56d5fa9d9ad48946619e0eb95755723fffa11022d75 \
+ --hash=sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4 \
+ --hash=sha256:8c952f69dd524558694818a461855f35d36cc7f5c0adddce37e962c85d06eac0 \
+ --hash=sha256:8f5fef13136c4e2dee91bfb9a44e236fff78fc2cd9f838eddfc470c3d7d90afe \
+ --hash=sha256:920b6bd77d209931e4c263223381d63f76828bec574440f29eb497cf3394c249 \
+ --hash=sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c \
+ --hash=sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576 \
+ --hash=sha256:9799257237d0479736e2b4c01ff26b5c7f7694ac9692a426cb717f3dc02fff9b \
+ --hash=sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770 \
+ --hash=sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046 \
+ --hash=sha256:9d124b38b3c299ca68433597ee26b7819209cb8a3a9ea761dfe9db3a04bba584 \
+ --hash=sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497 \
+ --hash=sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f \
+ --hash=sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f \
+ --hash=sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f \
+ --hash=sha256:ae8337990e7a45683548ffb2fee1af2f1ed08169284cd829cdd9a7fa7470530d \
+ --hash=sha256:b11534872256e1666116f6587a1592ef395a98b54476addb5e8d352925cb5d4a \
+ --hash=sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e \
+ --hash=sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68 \
+ --hash=sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9 \
+ --hash=sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8 \
+ --hash=sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1 \
+ --hash=sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0 \
+ --hash=sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29 \
+ --hash=sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0 \
+ --hash=sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215 \
+ --hash=sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590 \
+ --hash=sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c \
+ --hash=sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821 \
+ --hash=sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1 \
+ --hash=sha256:d3ceb265249fb401702fce3792e6b44c1166b9319737d21495d3611028d95769 \
+ --hash=sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506 \
+ --hash=sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3 \
+ --hash=sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348 \
+ --hash=sha256:e1c6bd2c6399920c9622362ce95a7d74e7f9af9bfec05fff91b8ce4b9647845a \
+ --hash=sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad \
+ --hash=sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6 \
+ --hash=sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45 \
+ --hash=sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188 \
+ --hash=sha256:e6e558ea1e47fd6fa8ac9ccdad403e5dd5ecc6ed8dda94343056fa4277d5c65e \
+ --hash=sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70 \
+ --hash=sha256:f1a39819a5a3e84304cd286e3dc62a549fe60985415851b3337b6f5cc91907f1 \
+ --hash=sha256:f27a9f9a86dcf00708be82359db8de86b80d029814e6693259befe82bb58a106 \
+ --hash=sha256:f2c7d5aa19714b1b01a0f515d078a629e445e667b9da869a3cd0e6fe7dec78bd \
+ --hash=sha256:f3a7bb0fe1f7a70fb5c6f497dc32619db7d2cdd53164af30ade2f34673f8b1fc \
+ --hash=sha256:f4b3cd7334a4bbc0c472164f3744562cb72d05002cc6fcf58adb104630bbc352 \
+ --hash=sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91 \
+ --hash=sha256:fb3b309f1d4086b5533cf7bbcf3f956f0ae6469664522f1bde4feed26fba60f1 \
+ --hash=sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f
+ # via
+ # aiohttp
+ # aiosignal
+google-auth==2.40.2 \
+ --hash=sha256:a33cde547a2134273226fa4b853883559947ebe9207521f7afc707efbf690f58 \
+ --hash=sha256:f7e568d42eedfded58734f6a60c58321896a621f7c116c411550a4b4a13da90b
+ # via google-auth-oauthlib
+google-auth-oauthlib==1.2.2 \
+ --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \
+ --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2
+ # via pipelex
+h11==0.16.0 \
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
+ # via httpcore
+httpcore==1.0.9 \
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
+ # via httpx
+httpx==0.28.1 \
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
+ # via
+ # anthropic
+ # fal-client
+ # mistralai
+ # openai
+ # pipelex
+httpx-sse==0.4.0 \
+ --hash=sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721 \
+ --hash=sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f
+ # via fal-client
+idna==3.10 \
+ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
+ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
+ # via
+ # anyio
+ # httpx
+ # requests
+ # yarl
+instructor==1.8.3 \
+ --hash=sha256:04d64ebc0d6e5eee104f4715b18fac1a02c21757ae6ce76efa65d38cbd536b0b \
+ --hash=sha256:a2c5066458132dc50ec6faa87cab9a2dd6b11b3f45c0e563a2ef704892286945
+ # via pipelex
+jinja2==3.1.6 \
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
+ # via
+ # instructor
+ # pipelex
+jiter==0.8.2 \
+ --hash=sha256:025337859077b41548bdcbabe38698bcd93cfe10b06ff66617a48ff92c9aec60 \
+ --hash=sha256:14601dcac4889e0a1c75ccf6a0e4baf70dbc75041e51bcf8d0e9274519df6887 \
+ --hash=sha256:180a8aea058f7535d1c84183c0362c710f4750bef66630c05f40c93c2b152a0f \
+ --hash=sha256:2dd61c5afc88a4fda7d8b2cf03ae5947c6ac7516d32b7a15bf4b49569a5c076b \
+ --hash=sha256:32475a42b2ea7b344069dc1e81445cfc00b9d0e3ca837f0523072432332e9f74 \
+ --hash=sha256:37b2998606d6dadbb5ccda959a33d6a5e853252d921fec1792fc902351bb4e2c \
+ --hash=sha256:3ac9f578c46f22405ff7f8b1f5848fb753cc4b8377fbec8470a7dc3997ca7566 \
+ --hash=sha256:3b94a33a241bee9e34b8481cdcaa3d5c2116f575e0226e421bed3f7a6ea71cff \
+ --hash=sha256:4a9220497ca0cb1fe94e3f334f65b9b5102a0b8147646118f020d8ce1de70105 \
+ --hash=sha256:4ab9a87f3784eb0e098f84a32670cfe4a79cb6512fd8f42ae3d0709f06405d18 \
+ --hash=sha256:5127dc1abd809431172bc3fbe8168d6b90556a30bb10acd5ded41c3cfd6f43b6 \
+ --hash=sha256:5672a86d55416ccd214c778efccf3266b84f87b89063b582167d803246354be4 \
+ --hash=sha256:580ccf358539153db147e40751a0b41688a5ceb275e6f3e93d91c9467f42b2e3 \
+ --hash=sha256:58dc9bc9767a1101f4e5e22db1b652161a225874d66f0e5cb8e2c7d1c438b587 \
+ --hash=sha256:5a90a923338531b7970abb063cfc087eebae6ef8ec8139762007188f6bc69a9f \
+ --hash=sha256:653cf462db4e8c41995e33d865965e79641ef45369d8a11f54cd30888b7e6ff1 \
+ --hash=sha256:66227a2c7b575720c1871c8800d3a0122bb8ee94edb43a5685aa9aceb2782d44 \
+ --hash=sha256:6e5337bf454abddd91bd048ce0dca5134056fc99ca0205258766db35d0a2ea43 \
+ --hash=sha256:711e408732d4e9a0208008e5892c2966b485c783cd2d9a681f3eb147cf36c7ef \
+ --hash=sha256:76e324da7b5da060287c54f2fabd3db5f76468006c811831f051942bf68c9d44 \
+ --hash=sha256:789361ed945d8d42850f919342a8665d2dc79e7e44ca1c97cc786966a21f627a \
+ --hash=sha256:79aec8172b9e3c6d05fd4b219d5de1ac616bd8da934107325a6c0d0e866a21b6 \
+ --hash=sha256:7efe4853ecd3d6110301665a5178b9856be7e2a9485f49d91aa4d737ad2ae49e \
+ --hash=sha256:83c0efd80b29695058d0fd2fa8a556490dbce9804eac3e281f373bbc99045f6c \
+ --hash=sha256:8b9931fd36ee513c26b5bf08c940b0ac875de175341cbdd4fa3be109f0492586 \
+ --hash=sha256:8ffc86ae5e3e6a93765d49d1ab47b6075a9c978a2b3b80f0f32628f39caa0c88 \
+ --hash=sha256:92249669925bc1c54fcd2ec73f70f2c1d6a817928480ee1c65af5f6b81cdf12d \
+ --hash=sha256:9c63eaef32b7bebac8ebebf4dabebdbc6769a09c127294db6babee38e9f405b9 \
+ --hash=sha256:a6c710d657c8d1d2adbbb5c0b0c6bfcec28fd35bd6b5f016395f9ac43e878a15 \
+ --hash=sha256:a9584de0cd306072635fe4b89742bf26feae858a0683b399ad0c2509011b9dc0 \
+ --hash=sha256:ab7f43235d71e03b941c1630f4b6e3055d46b6cb8728a17663eaac9d8e83a865 \
+ --hash=sha256:af102d3372e917cffce49b521e4c32c497515119dc7bd8a75665e90a718bbf08 \
+ --hash=sha256:b25bd626bde7fb51534190c7e3cb97cee89ee76b76d7585580e22f34f5e3f393 \
+ --hash=sha256:b2dd880785088ff2ad21ffee205e58a8c1ddabc63612444ae41e5e4b321b39c0 \
+ --hash=sha256:b426f72cd77da3fec300ed3bc990895e2dd6b49e3bfe6c438592a3ba660e41ca \
+ --hash=sha256:ba5bdf56969cad2019d4e8ffd3f879b5fdc792624129741d3d83fc832fef8c7d \
+ --hash=sha256:bf55846c7b7a680eebaf9c3c48d630e1bf51bdf76c68a5f654b8524335b0ad29 \
+ --hash=sha256:ca1f08b8e43dc3bd0594c992fb1fd2f7ce87f7bf0d44358198d6da8034afdf84 \
+ --hash=sha256:ca8577f6a413abe29b079bc30f907894d7eb07a865c4df69475e868d73e71c7b \
+ --hash=sha256:cadcc978f82397d515bb2683fc0d50103acff2a180552654bb92d6045dec2c49 \
+ --hash=sha256:cd73d3e740666d0e639f678adb176fad25c1bcbdae88d8d7b857e1783bb4212d \
+ --hash=sha256:cde031d8413842a1e7501e9129b8e676e62a657f8ec8166e18a70d94d4682855 \
+ --hash=sha256:ce0820f4a3a59ddced7fce696d86a096d5cc48d32a4183483a17671a61edfddc \
+ --hash=sha256:d21974d246ed0181558087cd9f76e84e8321091ebfb3a93d4c341479a736f099 \
+ --hash=sha256:d35c864c2dff13dfd79fb070fc4fc6235d7b9b359efe340e1261deb21b9fcb66 \
+ --hash=sha256:d5c826a221851a8dc028eb6d7d6429ba03184fa3c7e83ae01cd6d3bd1d4bd17d \
+ --hash=sha256:e6ec2be506e7d6f9527dae9ff4b7f54e68ea44a0ef6b098256ddf895218a2f8f \
+ --hash=sha256:e725edd0929fa79f8349ab4ec7f81c714df51dc4e991539a578e5018fa4a7152 \
+ --hash=sha256:eb21aaa9a200d0a80dacc7a81038d2e476ffe473ffdd9c91eb745d623561de05 \
+ --hash=sha256:ecff0dc14f409599bbcafa7e470c00b80f17abc14d1405d38ab02e4b42e55b57 \
+ --hash=sha256:f557c55bc2b7676e74d39d19bcb8775ca295c7a028246175d6a8b431e70835e5 \
+ --hash=sha256:ffd9fee7d0775ebaba131f7ca2e2d83839a62ad65e8e02fe2bd8fc975cedeb9e
+ # via
+ # anthropic
+ # instructor
+ # openai
+jmespath==1.0.1 \
+ --hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \
+ --hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe
+ # via
+ # aiobotocore
+ # boto3
+ # botocore
+json2html==1.3.0 \
+ --hash=sha256:8951a53662ae9cfd812685facdba693fc950ffc1c1fd1a8a2d3cf4c34600689c
+ # via pipelex
+jsonpath-python==1.0.6 \
+ --hash=sha256:1e3b78df579f5efc23565293612decee04214609208a2335884b3ee3f786b575 \
+ --hash=sha256:dd5be4a72d8a2995c3f583cf82bf3cd1a9544cfdabf2d22595b67aff07349666
+ # via mistralai
+kajson==0.3.0 \
+ --hash=sha256:5ea8e164cd7fc96877bf4038cf64c286ac4606814146c24dc9ecc4a2c6ac047d \
+ --hash=sha256:d8cc72875ebef6ea44c06c96fe63accf5b0721ca24ea1e593814dd45fd25bf15
+ # via pipelex
+markdown==3.8 \
+ --hash=sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc \
+ --hash=sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f
+ # via pipelex
+markdown-it-py==3.0.0 \
+ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
+ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
+ # via rich
+markupsafe==3.0.2 \
+ --hash=sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4 \
+ --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
+ --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \
+ --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \
+ --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
+ --hash=sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca \
+ --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
+ --hash=sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832 \
+ --hash=sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b \
+ --hash=sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579 \
+ --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \
+ --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \
+ --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \
+ --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
+ --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \
+ --hash=sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb \
+ --hash=sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e \
+ --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \
+ --hash=sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a \
+ --hash=sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d \
+ --hash=sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b \
+ --hash=sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8 \
+ --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
+ --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
+ --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
+ --hash=sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d \
+ --hash=sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93 \
+ --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
+ --hash=sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158 \
+ --hash=sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84 \
+ --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \
+ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
+ --hash=sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171 \
+ --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \
+ --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \
+ --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \
+ --hash=sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d \
+ --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \
+ --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \
+ --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \
+ --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \
+ --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \
+ --hash=sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798 \
+ --hash=sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c \
+ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
+ --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \
+ --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \
+ --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \
+ --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \
+ --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 \
+ --hash=sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50
+ # via jinja2
+mdurl==0.1.2 \
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
+ # via markdown-it-py
+mistralai==1.5.2 \
+ --hash=sha256:5b1112acebbcad1afd7732ce0bd60614975b64999801c555c54768ac41f506ae \
+ --hash=sha256:f39e6e51e8939aac2602e4badcb18712cbee2df33d86100c559333e609b92d17
+ # via pipelex
+multidict==6.4.4 \
+ --hash=sha256:0327ad2c747a6600e4797d115d3c38a220fdb28e54983abe8964fd17e95ae83c \
+ --hash=sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0 \
+ --hash=sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c \
+ --hash=sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd \
+ --hash=sha256:0f14ea68d29b43a9bf37953881b1e3eb75b2739e896ba4a6aa4ad4c5b9ffa145 \
+ --hash=sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f \
+ --hash=sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c \
+ --hash=sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1 \
+ --hash=sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2 \
+ --hash=sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e \
+ --hash=sha256:33a12ebac9f380714c298cbfd3e5b9c0c4e89c75fe612ae496512ee51028915f \
+ --hash=sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49 \
+ --hash=sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd \
+ --hash=sha256:4efc31dfef8c4eeb95b6b17d799eedad88c4902daba39ce637e23a17ea078915 \
+ --hash=sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95 \
+ --hash=sha256:4ffc3c6a37e048b5395ee235e4a2a0d639c2349dffa32d9367a42fc20d399772 \
+ --hash=sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51 \
+ --hash=sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e \
+ --hash=sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275 \
+ --hash=sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42 \
+ --hash=sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd \
+ --hash=sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601 \
+ --hash=sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44 \
+ --hash=sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781 \
+ --hash=sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b \
+ --hash=sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3 \
+ --hash=sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de \
+ --hash=sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031 \
+ --hash=sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8 \
+ --hash=sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf \
+ --hash=sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156 \
+ --hash=sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9 \
+ --hash=sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529 \
+ --hash=sha256:75493f28dbadecdbb59130e74fe935288813301a8554dc32f0c631b6bdcdf8b0 \
+ --hash=sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780 \
+ --hash=sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc \
+ --hash=sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9 \
+ --hash=sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f \
+ --hash=sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed \
+ --hash=sha256:87cb72263946b301570b0f63855569a24ee8758aaae2cd182aae7d95fbc92ca7 \
+ --hash=sha256:8adee3ac041145ffe4488ea73fa0a622b464cc25340d98be76924d0cda8545ff \
+ --hash=sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1 \
+ --hash=sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373 \
+ --hash=sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c \
+ --hash=sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a \
+ --hash=sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d \
+ --hash=sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69 \
+ --hash=sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15 \
+ --hash=sha256:9bbf7bd39822fd07e3609b6b4467af4c404dd2b88ee314837ad1830a7f4a8299 \
+ --hash=sha256:9c17341ee04545fd962ae07330cb5a39977294c883485c8d74634669b1f7fe04 \
+ --hash=sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740 \
+ --hash=sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065 \
+ --hash=sha256:9fcad2945b1b91c29ef2b4050f590bfcb68d8ac8e0995a74e659aa57e8d78e01 \
+ --hash=sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e \
+ --hash=sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f \
+ --hash=sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26 \
+ --hash=sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1 \
+ --hash=sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a \
+ --hash=sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b \
+ --hash=sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08 \
+ --hash=sha256:b61e98c3e2a861035aaccd207da585bdcacef65fe01d7a0d07478efac005e028 \
+ --hash=sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93 \
+ --hash=sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb \
+ --hash=sha256:bb5ac9e5bfce0e6282e7f59ff7b7b9a74aa8e5c60d38186a4637f5aa764046ad \
+ --hash=sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223 \
+ --hash=sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20 \
+ --hash=sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac \
+ --hash=sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e \
+ --hash=sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a \
+ --hash=sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab \
+ --hash=sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4 \
+ --hash=sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0 \
+ --hash=sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd \
+ --hash=sha256:d1a20707492db9719a05fc62ee215fd2c29b22b47c1b1ba347f9abc831e26683 \
+ --hash=sha256:d1f7cbd4f1f44ddf5fd86a8675b7679176eae770f2fc88115d6dddb6cefb59bc \
+ --hash=sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645 \
+ --hash=sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e \
+ --hash=sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925 \
+ --hash=sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0 \
+ --hash=sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046 \
+ --hash=sha256:d83f18315b9fca5db2452d1881ef20f79593c4aa824095b62cb280019ef7aa3d \
+ --hash=sha256:d877447e7368c7320832acb7159557e49b21ea10ffeb135c1077dbbc0816b598 \
+ --hash=sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2 \
+ --hash=sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2 \
+ --hash=sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2 \
+ --hash=sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b \
+ --hash=sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482
+ # via
+ # aiobotocore
+ # aiohttp
+ # yarl
+mypy-extensions==1.1.0 \
+ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
+ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
+ # via typing-inspect
+networkx==3.4.2 ; python_full_version < '3.11' \
+ --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \
+ --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f
+ # via pipelex
+networkx==3.5 ; python_full_version >= '3.11' \
+ --hash=sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec \
+ --hash=sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037
+ # via pipelex
+numpy==2.2.6 \
+ --hash=sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff \
+ --hash=sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47 \
+ --hash=sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84 \
+ --hash=sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d \
+ --hash=sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6 \
+ --hash=sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f \
+ --hash=sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b \
+ --hash=sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49 \
+ --hash=sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163 \
+ --hash=sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571 \
+ --hash=sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42 \
+ --hash=sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff \
+ --hash=sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491 \
+ --hash=sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4 \
+ --hash=sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566 \
+ --hash=sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf \
+ --hash=sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40 \
+ --hash=sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd \
+ --hash=sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06 \
+ --hash=sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282 \
+ --hash=sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680 \
+ --hash=sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db \
+ --hash=sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3 \
+ --hash=sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90 \
+ --hash=sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1 \
+ --hash=sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289 \
+ --hash=sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab \
+ --hash=sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c \
+ --hash=sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d \
+ --hash=sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb \
+ --hash=sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d \
+ --hash=sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a \
+ --hash=sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf \
+ --hash=sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1 \
+ --hash=sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2 \
+ --hash=sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a \
+ --hash=sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543 \
+ --hash=sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00 \
+ --hash=sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c \
+ --hash=sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f \
+ --hash=sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd \
+ --hash=sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868 \
+ --hash=sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303 \
+ --hash=sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83 \
+ --hash=sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3 \
+ --hash=sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d \
+ --hash=sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87 \
+ --hash=sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa \
+ --hash=sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f \
+ --hash=sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae \
+ --hash=sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda \
+ --hash=sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915 \
+ --hash=sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249 \
+ --hash=sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de \
+ --hash=sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8
+ # via pandas
+oauthlib==3.2.2 \
+ --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \
+ --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918
+ # via requests-oauthlib
+openai==1.82.1 \
+ --hash=sha256:334eb5006edf59aa464c9e932b9d137468d810b2659e5daea9b3a8c39d052395 \
+ --hash=sha256:ffc529680018e0417acac85f926f92aa0bbcbc26e82e2621087303c66bc7f95d
+ # via
+ # instructor
+ # pipelex
+openpyxl==3.1.5 \
+ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \
+ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050
+ # via pipelex
+pandas==2.2.3 \
+ --hash=sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a \
+ --hash=sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d \
+ --hash=sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5 \
+ --hash=sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4 \
+ --hash=sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0 \
+ --hash=sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32 \
+ --hash=sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28 \
+ --hash=sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f \
+ --hash=sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348 \
+ --hash=sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18 \
+ --hash=sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468 \
+ --hash=sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5 \
+ --hash=sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667 \
+ --hash=sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645 \
+ --hash=sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13 \
+ --hash=sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3 \
+ --hash=sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d \
+ --hash=sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb \
+ --hash=sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3 \
+ --hash=sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039 \
+ --hash=sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8 \
+ --hash=sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd \
+ --hash=sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659 \
+ --hash=sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57 \
+ --hash=sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4 \
+ --hash=sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a \
+ --hash=sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9 \
+ --hash=sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42 \
+ --hash=sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2 \
+ --hash=sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc \
+ --hash=sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698 \
+ --hash=sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed \
+ --hash=sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015 \
+ --hash=sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24 \
+ --hash=sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319
+ # via pipelex
+pillow==11.2.1 \
+ --hash=sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b \
+ --hash=sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91 \
+ --hash=sha256:0c3e6d0f59171dfa2e25d7116217543310908dfa2770aa64b8f87605f8cacc97 \
+ --hash=sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4 \
+ --hash=sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193 \
+ --hash=sha256:127bf6ac4a5b58b3d32fc8289656f77f80567d65660bc46f72c0d77e6600cc95 \
+ --hash=sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941 \
+ --hash=sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f \
+ --hash=sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f \
+ --hash=sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3 \
+ --hash=sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044 \
+ --hash=sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb \
+ --hash=sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681 \
+ --hash=sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d \
+ --hash=sha256:2728567e249cdd939f6cc3d1f049595c66e4187f3c34078cbc0a7d21c47482d2 \
+ --hash=sha256:312c77b7f07ab2139924d2639860e084ec2a13e72af54d4f08ac843a5fc9c79d \
+ --hash=sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406 \
+ --hash=sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70 \
+ --hash=sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e \
+ --hash=sha256:39ad2e0f424394e3aebc40168845fee52df1394a4673a6ee512d840d14ab3013 \
+ --hash=sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d \
+ --hash=sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2 \
+ --hash=sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7 \
+ --hash=sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751 \
+ --hash=sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c \
+ --hash=sha256:562d11134c97a62fe3af29581f083033179f7ff435f78392565a1ad2d1c2c45c \
+ --hash=sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c \
+ --hash=sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b \
+ --hash=sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd \
+ --hash=sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691 \
+ --hash=sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14 \
+ --hash=sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b \
+ --hash=sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f \
+ --hash=sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0 \
+ --hash=sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed \
+ --hash=sha256:85d27ea4c889342f7e35f6d56e7e1cb345632ad592e8c51b693d7b7556043ce0 \
+ --hash=sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22 \
+ --hash=sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788 \
+ --hash=sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16 \
+ --hash=sha256:9b7b0d4fd2635f54ad82785d56bc0d94f147096493a79985d0ab57aedd563156 \
+ --hash=sha256:9bc7ae48b8057a611e5fe9f853baa88093b9a76303937449397899385da06fad \
+ --hash=sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076 \
+ --hash=sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7 \
+ --hash=sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e \
+ --hash=sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6 \
+ --hash=sha256:aa442755e31c64037aa7c1cb186e0b369f8416c567381852c63444dd666fb772 \
+ --hash=sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155 \
+ --hash=sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830 \
+ --hash=sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4 \
+ --hash=sha256:b4ba4be812c7a40280629e55ae0b14a0aafa150dd6451297562e1764808bbe61 \
+ --hash=sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8 \
+ --hash=sha256:bf2c33d6791c598142f00c9c4c7d47f6476731c31081331664eb26d6ab583e01 \
+ --hash=sha256:c8bd62331e5032bc396a93609982a9ab6b411c05078a52f5fe3cc59234a3abd1 \
+ --hash=sha256:c97209e85b5be259994eb5b69ff50c5d20cca0f458ef9abd835e262d9d88b39d \
+ --hash=sha256:cc1c3bc53befb6096b84165956e886b1729634a799e9d6329a0c512ab651e579 \
+ --hash=sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6 \
+ --hash=sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1 \
+ --hash=sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7 \
+ --hash=sha256:d57a75d53922fc20c165016a20d9c44f73305e67c351bbc60d1adaf662e74047 \
+ --hash=sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443 \
+ --hash=sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf \
+ --hash=sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd \
+ --hash=sha256:e616e7154c37669fc1dfc14584f11e284e05d1c650e1c0f972f281c4ccc53193 \
+ --hash=sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600 \
+ --hash=sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c \
+ --hash=sha256:f0d3348c95b766f54b76116d53d4cb171b52992a1027e7ca50c81b43b9d9e363 \
+ --hash=sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e \
+ --hash=sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9 \
+ --hash=sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28 \
+ --hash=sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b
+ # via pipelex
+pipelex @ git+https://github.com/Pipelex/pipelex.git@9d93639f6abe0beeb485138a78ae14042537cf17
+ # via my-project
+polyfactory==2.21.0 \
+ --hash=sha256:9483b764756c8622313d99f375889b1c0d92f09affb05742d7bcfa2b5198d8c5 \
+ --hash=sha256:a6d8dba91b2515d744cc014b5be48835633f7ccb72519a68f8801759e5b1737a
+ # via pipelex
+propcache==0.3.1 \
+ --hash=sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e \
+ --hash=sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b \
+ --hash=sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b \
+ --hash=sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c \
+ --hash=sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a \
+ --hash=sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf \
+ --hash=sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8 \
+ --hash=sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5 \
+ --hash=sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42 \
+ --hash=sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0 \
+ --hash=sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e \
+ --hash=sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46 \
+ --hash=sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d \
+ --hash=sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24 \
+ --hash=sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d \
+ --hash=sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de \
+ --hash=sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf \
+ --hash=sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7 \
+ --hash=sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371 \
+ --hash=sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833 \
+ --hash=sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259 \
+ --hash=sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136 \
+ --hash=sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25 \
+ --hash=sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005 \
+ --hash=sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef \
+ --hash=sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7 \
+ --hash=sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f \
+ --hash=sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53 \
+ --hash=sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0 \
+ --hash=sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb \
+ --hash=sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a \
+ --hash=sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458 \
+ --hash=sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9 \
+ --hash=sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71 \
+ --hash=sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b \
+ --hash=sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5 \
+ --hash=sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037 \
+ --hash=sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757 \
+ --hash=sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3 \
+ --hash=sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976 \
+ --hash=sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6 \
+ --hash=sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7 \
+ --hash=sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649 \
+ --hash=sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120 \
+ --hash=sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd \
+ --hash=sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40 \
+ --hash=sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e \
+ --hash=sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229 \
+ --hash=sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c \
+ --hash=sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7 \
+ --hash=sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111 \
+ --hash=sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654 \
+ --hash=sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f \
+ --hash=sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da \
+ --hash=sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f \
+ --hash=sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7 \
+ --hash=sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0 \
+ --hash=sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073 \
+ --hash=sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11 \
+ --hash=sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f \
+ --hash=sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27 \
+ --hash=sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70 \
+ --hash=sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7 \
+ --hash=sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519 \
+ --hash=sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5 \
+ --hash=sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180 \
+ --hash=sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f \
+ --hash=sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee \
+ --hash=sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18 \
+ --hash=sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815 \
+ --hash=sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e \
+ --hash=sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a \
+ --hash=sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7 \
+ --hash=sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc \
+ --hash=sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8 \
+ --hash=sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98 \
+ --hash=sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256 \
+ --hash=sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5 \
+ --hash=sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744 \
+ --hash=sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723 \
+ --hash=sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277 \
+ --hash=sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5
+ # via
+ # aiohttp
+ # yarl
+pyasn1==0.6.1 \
+ --hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
+ --hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
+ # via
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.4.2 \
+ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \
+ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6
+ # via google-auth
+pydantic==2.10.6 \
+ --hash=sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584 \
+ --hash=sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236
+ # via
+ # anthropic
+ # instructor
+ # kajson
+ # mistralai
+ # openai
+ # pipelex
+pydantic-core==2.27.2 \
+ --hash=sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50 \
+ --hash=sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9 \
+ --hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \
+ --hash=sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc \
+ --hash=sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9 \
+ --hash=sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236 \
+ --hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \
+ --hash=sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee \
+ --hash=sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b \
+ --hash=sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048 \
+ --hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \
+ --hash=sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130 \
+ --hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \
+ --hash=sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4 \
+ --hash=sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7 \
+ --hash=sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7 \
+ --hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \
+ --hash=sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e \
+ --hash=sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa \
+ --hash=sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6 \
+ --hash=sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962 \
+ --hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \
+ --hash=sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f \
+ --hash=sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474 \
+ --hash=sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5 \
+ --hash=sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459 \
+ --hash=sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a \
+ --hash=sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c \
+ --hash=sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4 \
+ --hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \
+ --hash=sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306 \
+ --hash=sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3 \
+ --hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \
+ --hash=sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af \
+ --hash=sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9 \
+ --hash=sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a \
+ --hash=sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27 \
+ --hash=sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b \
+ --hash=sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151 \
+ --hash=sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154 \
+ --hash=sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133 \
+ --hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \
+ --hash=sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15 \
+ --hash=sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4 \
+ --hash=sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc \
+ --hash=sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee \
+ --hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \
+ --hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \
+ --hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \
+ --hash=sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b \
+ --hash=sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8 \
+ --hash=sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1 \
+ --hash=sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e \
+ --hash=sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc \
+ --hash=sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c \
+ --hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \
+ --hash=sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1 \
+ --hash=sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d \
+ --hash=sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99 \
+ --hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \
+ --hash=sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31 \
+ --hash=sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c \
+ --hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39 \
+ --hash=sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a \
+ --hash=sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9
+ # via
+ # instructor
+ # pydantic
+pygments==2.19.1 \
+ --hash=sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f \
+ --hash=sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c
+ # via rich
+pypdfium2==4.30.0 \
+ --hash=sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e \
+ --hash=sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29 \
+ --hash=sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2 \
+ --hash=sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16 \
+ --hash=sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de \
+ --hash=sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854 \
+ --hash=sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163 \
+ --hash=sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c \
+ --hash=sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab \
+ --hash=sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad \
+ --hash=sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e \
+ --hash=sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f \
+ --hash=sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be
+ # via pipelex
+python-dateutil==2.9.0.post0 \
+ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
+ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
+ # via
+ # aiobotocore
+ # botocore
+ # mistralai
+ # pandas
+python-dotenv==1.1.0 \
+ --hash=sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5 \
+ --hash=sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d
+ # via pipelex
+pytz==2025.2 \
+ --hash=sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3 \
+ --hash=sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00
+ # via pandas
+pyyaml==6.0.2 \
+ --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
+ --hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
+ --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
+ --hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
+ --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
+ --hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
+ --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
+ --hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
+ --hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
+ --hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
+ --hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
+ --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
+ --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
+ --hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
+ --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
+ --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
+ --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
+ --hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
+ --hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
+ --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
+ --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
+ --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
+ --hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
+ --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
+ --hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
+ --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
+ --hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
+ --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
+ --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
+ --hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
+ --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
+ --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
+ --hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
+ --hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
+ --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
+ --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
+ --hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
+ # via pipelex
+requests==2.32.3 \
+ --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
+ --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
+ # via
+ # instructor
+ # requests-oauthlib
+requests-oauthlib==2.0.0 \
+ --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \
+ --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9
+ # via google-auth-oauthlib
+rich==13.9.4 \
+ --hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \
+ --hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90
+ # via
+ # instructor
+ # pipelex
+ # typer
+rsa==4.9.1 \
+ --hash=sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762 \
+ --hash=sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75
+ # via google-auth
+s3transfer==0.11.3 \
+ --hash=sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d \
+ --hash=sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a
+ # via boto3
+shellingham==1.5.4 \
+ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
+ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
+ # via typer
+shortuuid==1.0.13 \
+ --hash=sha256:3bb9cf07f606260584b1df46399c0b87dd84773e7b25912b7e391e30797c5e72 \
+ --hash=sha256:a482a497300b49b4953e15108a7913244e1bb0d41f9d332f5e9925dba33a3c5a
+ # via pipelex
+six==1.17.0 \
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+ # via python-dateutil
+sniffio==1.3.1 \
+ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
+ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
+ # via
+ # anthropic
+ # anyio
+ # openai
+tenacity==9.1.2 \
+ --hash=sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb \
+ --hash=sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138
+ # via instructor
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via pipelex
+tomlkit==0.13.3 \
+ --hash=sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1 \
+ --hash=sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0
+ # via pipelex
+tqdm==4.67.1 \
+ --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \
+ --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2
+ # via openai
+typer==0.16.0 \
+ --hash=sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855 \
+ --hash=sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b
+ # via
+ # instructor
+ # pipelex
+typing-extensions==4.13.2 \
+ --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \
+ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef
+ # via
+ # anthropic
+ # anyio
+ # exceptiongroup
+ # multidict
+ # openai
+ # pipelex
+ # polyfactory
+ # pydantic
+ # pydantic-core
+ # rich
+ # typer
+ # typing-inspect
+typing-inspect==0.9.0 \
+ --hash=sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f \
+ --hash=sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78
+ # via mistralai
+tzdata==2025.2 \
+ --hash=sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 \
+ --hash=sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9
+ # via
+ # faker
+ # pandas
+urllib3==2.4.0 \
+ --hash=sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466 \
+ --hash=sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813
+ # via
+ # botocore
+ # requests
+wrapt==1.17.2 \
+ --hash=sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c \
+ --hash=sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a \
+ --hash=sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b \
+ --hash=sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555 \
+ --hash=sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c \
+ --hash=sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b \
+ --hash=sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6 \
+ --hash=sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662 \
+ --hash=sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998 \
+ --hash=sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62 \
+ --hash=sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984 \
+ --hash=sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392 \
+ --hash=sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2 \
+ --hash=sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306 \
+ --hash=sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3 \
+ --hash=sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9 \
+ --hash=sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6 \
+ --hash=sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192 \
+ --hash=sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317 \
+ --hash=sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f \
+ --hash=sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda \
+ --hash=sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563 \
+ --hash=sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f \
+ --hash=sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d \
+ --hash=sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8 \
+ --hash=sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845 \
+ --hash=sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82 \
+ --hash=sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125 \
+ --hash=sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504 \
+ --hash=sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b \
+ --hash=sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7 \
+ --hash=sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc \
+ --hash=sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6 \
+ --hash=sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40 \
+ --hash=sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72 \
+ --hash=sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681 \
+ --hash=sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438 \
+ --hash=sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae \
+ --hash=sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2 \
+ --hash=sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb \
+ --hash=sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5 \
+ --hash=sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a \
+ --hash=sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3 \
+ --hash=sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8 \
+ --hash=sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22 \
+ --hash=sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72 \
+ --hash=sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061 \
+ --hash=sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98 \
+ --hash=sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b \
+ --hash=sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925 \
+ --hash=sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6 \
+ --hash=sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0 \
+ --hash=sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9 \
+ --hash=sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c \
+ --hash=sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991 \
+ --hash=sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000 \
+ --hash=sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58
+ # via aiobotocore
+yarl==1.20.0 \
+ --hash=sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9 \
+ --hash=sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61 \
+ --hash=sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2 \
+ --hash=sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2 \
+ --hash=sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33 \
+ --hash=sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902 \
+ --hash=sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2 \
+ --hash=sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0 \
+ --hash=sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0 \
+ --hash=sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569 \
+ --hash=sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f \
+ --hash=sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7 \
+ --hash=sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00 \
+ --hash=sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1 \
+ --hash=sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f \
+ --hash=sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd \
+ --hash=sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c \
+ --hash=sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f \
+ --hash=sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5 \
+ --hash=sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d \
+ --hash=sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501 \
+ --hash=sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3 \
+ --hash=sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2 \
+ --hash=sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c \
+ --hash=sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae \
+ --hash=sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de \
+ --hash=sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a \
+ --hash=sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe \
+ --hash=sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8 \
+ --hash=sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124 \
+ --hash=sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb \
+ --hash=sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc \
+ --hash=sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2 \
+ --hash=sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac \
+ --hash=sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307 \
+ --hash=sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58 \
+ --hash=sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e \
+ --hash=sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e \
+ --hash=sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62 \
+ --hash=sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b \
+ --hash=sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe \
+ --hash=sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda \
+ --hash=sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5 \
+ --hash=sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64 \
+ --hash=sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229 \
+ --hash=sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62 \
+ --hash=sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6 \
+ --hash=sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d \
+ --hash=sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791 \
+ --hash=sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672 \
+ --hash=sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb \
+ --hash=sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a \
+ --hash=sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10 \
+ --hash=sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e \
+ --hash=sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9 \
+ --hash=sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5 \
+ --hash=sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da \
+ --hash=sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a \
+ --hash=sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d \
+ --hash=sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195 \
+ --hash=sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594 \
+ --hash=sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8 \
+ --hash=sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634 \
+ --hash=sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051 \
+ --hash=sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e \
+ --hash=sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384 \
+ --hash=sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076 \
+ --hash=sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018 \
+ --hash=sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19 \
+ --hash=sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a \
+ --hash=sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4 \
+ --hash=sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2 \
+ --hash=sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145 \
+ --hash=sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995 \
+ --hash=sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6 \
+ --hash=sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f \
+ --hash=sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f \
+ --hash=sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487 \
+ --hash=sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9 \
+ --hash=sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a \
+ --hash=sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f \
+ --hash=sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1 \
+ --hash=sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22 \
+ --hash=sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d \
+ --hash=sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877 \
+ --hash=sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867 \
+ --hash=sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3
+ # via aiohttp
+yattag==1.16.1 \
+ --hash=sha256:baa8f254e7ea5d3e0618281ad2ff5610e0e5360b3608e695c29bfb3b29d051f4
+ # via pipelex
diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py
index 613370b..f7016d1 100644
--- a/tests/e2e/conftest.py
+++ b/tests/e2e/conftest.py
@@ -2,7 +2,7 @@
import pipelex.pipelex
import pytest
-from pipelex.core.pipes.pipe_run_params import FORCE_DRY_RUN_MODE_ENV_KEY, PipeRunMode
+from pipelex.pipe_run.pipe_run_params import FORCE_DRY_RUN_MODE_ENV_KEY, PipeRunMode
from rich import print
diff --git a/tests/integration/test_fundamentals.py b/tests/integration/test_fundamentals.py
index 9e07d67..9917b94 100644
--- a/tests/integration/test_fundamentals.py
+++ b/tests/integration/test_fundamentals.py
@@ -1,5 +1,6 @@
import pytest
-from pipelex.pipe_works.pipe_dry import dry_run_all_pipes
+from pipelex.hub import get_pipes
+from pipelex.pipe_run.dry_run import DryRunStatus, dry_run_pipes
from pipelex.pipelex import Pipelex
@@ -16,4 +17,12 @@ def test_validate_libraries(self):
@pytest.mark.asyncio(loop_scope="class")
async def test_dry_run_all_pipes(self):
- await dry_run_all_pipes()
+ results = await dry_run_pipes(pipes=get_pipes(), raise_on_failure=False)
+
+ # Check if there were any failures
+
+ failed_pipes = {pipe_code: output for pipe_code, output in results.items() if output.status == DryRunStatus.FAILURE}
+
+ if failed_pipes:
+ failure_details = "\n".join([f" - {pipe_code}: {output.error_message}" for pipe_code, output in failed_pipes.items()])
+ pytest.fail(f"Dry run failed for {len(failed_pipes)} pipes:\n{failure_details}")
diff --git a/uv.lock b/uv.lock
index 7c5aa87..69fff16 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,7 +2,9 @@ version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
- "python_full_version >= '3.12'",
+ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.12' and platform_python_implementation == 'PyPy'",
"python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
@@ -179,20 +181,21 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.52.1"
+version = "0.69.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
+ { name = "docstring-parser" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/6b/7d632b680556812f36b663891ccab8f5bf71b61913a3c7332aa7aa2822be/anthropic-0.52.1.tar.gz", hash = "sha256:da4a7c3aeac0170cb45a42dc3369ca1fcf2b3238edf845cb056505d4b0c42fcf", size = 241807, upload-time = "2025-05-28T13:13:40.851Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/9ad1778b95f15c5b04e7d328c1b5f558f1e893857b7c33cd288c19c0057a/anthropic-0.69.0.tar.gz", hash = "sha256:c604d287f4d73640f40bd2c0f3265a2eb6ce034217ead0608f6b07a8bc5ae5f2", size = 480622, upload-time = "2025-09-29T16:53:45.282Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9d/e7/36ad2dc22811ea004e1f511b2f5604fe71a56bdfd3b969b265892d45affa/anthropic-0.52.1-py3-none-any.whl", hash = "sha256:807cee7ebc5503753da0403a77932decf5a4c036041ddda58b4edcdb2a3da551", size = 286076, upload-time = "2025-05-28T13:13:39.052Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/38/75129688de5637eb5b383e5f2b1570a5cc3aecafa4de422da8eea4b90a6c/anthropic-0.69.0-py3-none-any.whl", hash = "sha256:1f73193040f33f11e27c2cd6ec25f24fe7c3f193dc1c5cde6b7a08b18a16bcc5", size = 337265, upload-time = "2025-09-29T16:53:43.686Z" },
]
[[package]]
@@ -309,63 +312,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
-[[package]]
-name = "cffi"
-version = "1.17.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" },
- { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" },
- { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" },
- { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" },
- { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" },
- { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" },
- { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" },
- { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" },
- { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" },
- { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
- { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
- { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
- { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
- { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
- { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
- { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
- { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
- { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
- { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
- { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
- { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
- { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
- { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
- { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
- { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
- { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
- { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
- { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
- { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
- { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
- { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
- { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
- { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
- { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
- { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
- { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
- { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
- { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
- { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
- { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
-]
-
[[package]]
name = "charset-normalizer"
version = "3.4.2"
@@ -439,19 +385,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
-[[package]]
-name = "cocode"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pipelex", extra = ["anthropic", "bedrock", "google"] },
- { name = "pygithub" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/08/a8/890526fa2f3cab311cb7a6d56fedd56c6829254fe13ca58435883f14c21f/cocode-0.1.2.tar.gz", hash = "sha256:63b7f9c51bd18c55f485051d714577a5b755498a20e6a9c69fa9ed734fb47801", size = 43905, upload-time = "2025-09-03T09:14:33.233Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/56/e37fd29be59dca658a22aef6328f475850ff4b8cc988e60e04ccdc9fb2b4/cocode-0.1.2-py3-none-any.whl", hash = "sha256:32aba620170da4e1855394fdf37274bf443a98fba98f0d5da1d89a06f06aa8c6", size = 60516, upload-time = "2025-09-03T09:14:32.072Z" },
-]
-
[[package]]
name = "colorama"
version = "0.4.6"
@@ -461,65 +394,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
-[[package]]
-name = "cryptography"
-version = "45.0.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" },
- { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" },
- { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" },
- { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" },
- { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" },
- { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" },
- { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" },
- { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" },
- { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" },
- { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" },
- { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" },
- { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" },
- { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" },
- { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" },
- { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" },
- { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" },
- { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" },
- { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" },
- { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" },
- { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" },
- { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" },
- { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" },
- { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" },
- { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" },
- { url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442, upload-time = "2025-09-01T11:14:39.836Z" },
- { url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233, upload-time = "2025-09-01T11:14:41.305Z" },
- { url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202, upload-time = "2025-09-01T11:14:43.047Z" },
- { url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900, upload-time = "2025-09-01T11:14:45.089Z" },
- { url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562, upload-time = "2025-09-01T11:14:47.166Z" },
- { url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781, upload-time = "2025-09-01T11:14:48.747Z" },
- { url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634, upload-time = "2025-09-01T11:14:50.593Z" },
- { url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533, upload-time = "2025-09-01T11:14:52.091Z" },
- { url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557, upload-time = "2025-09-01T11:14:53.551Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023, upload-time = "2025-09-01T11:14:55.022Z" },
- { url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722, upload-time = "2025-09-01T11:14:57.319Z" },
- { url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908, upload-time = "2025-09-01T11:14:58.78Z" },
-]
-
-[[package]]
-name = "deprecated"
-version = "1.2.18"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
-]
-
[[package]]
name = "distro"
version = "1.9.0"
@@ -538,15 +412,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" },
]
-[[package]]
-name = "et-xmlfile"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
-]
-
[[package]]
name = "eval-type-backport"
version = "0.2.2"
@@ -723,6 +588,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/84/40ee070be95771acd2f4418981edb834979424565c3eec3cd88b6aa09d24/google_auth_oauthlib-1.2.2-py3-none-any.whl", hash = "sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2", size = 19072, upload-time = "2025-04-22T16:40:28.174Z" },
]
+[[package]]
+name = "google-genai"
+version = "1.41.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "google-auth" },
+ { name = "httpx" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/8b/ee20bcf707769b3b0e1106c3b5c811507736af7e8a60f29a70af1750ba19/google_genai-1.41.0.tar.gz", hash = "sha256:134f861bb0ace4e34af0501ecb75ceee15f7662fd8120698cd185e8cb39f2800", size = 245812, upload-time = "2025-10-02T22:30:29.699Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/14/e5e8fbca8863fee718208566c4e927b8e9f45fd46ec5cf89e24759da545b/google_genai-1.41.0-py3-none-any.whl", hash = "sha256:111a3ee64c1a0927d3879faddb368234594432479a40c311e5fe4db338ca8778", size = 245931, upload-time = "2025-10-02T22:30:27.885Z" },
+]
+
[[package]]
name = "h11"
version = "0.16.0"
@@ -809,6 +693,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/76/0e198373f182020559b0886d0d61dd2ddca109ab7a9eddf5458ff759d51a/instructor-1.8.3-py3-none-any.whl", hash = "sha256:a2c5066458132dc50ec6faa87cab9a2dd6b11b3f45c0e563a2ef704892286945", size = 94619, upload-time = "2025-05-22T16:43:58.529Z" },
]
+[package.optional-dependencies]
+google-genai = [
+ { name = "google-genai" },
+ { name = "jsonref" },
+]
+
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -904,16 +794,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/8a/d63959f4eff03893a00e6e63592e3a9f15b9266ed8e0275ab77f8c7dbc94/jsonpath_python-1.0.6-py3-none-any.whl", hash = "sha256:1e3b78df579f5efc23565293612decee04214609208a2335884b3ee3f786b575", size = 7552, upload-time = "2022-03-14T02:34:59.754Z" },
]
+[[package]]
+name = "jsonref"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
+]
+
[[package]]
name = "kajson"
-version = "0.3.0"
+version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/b3/51/83d17d4fc6db6b9d497f91c6cbdf7c5677116797a44d0ad65960f1b66c90/kajson-0.3.0.tar.gz", hash = "sha256:5ea8e164cd7fc96877bf4038cf64c286ac4606814146c24dc9ecc4a2c6ac047d", size = 20271, upload-time = "2025-07-09T09:52:33.851Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/51/dc0529d2b678003e783cc9e79b0ca16db1ca1035935a5623deb822b33b8f/kajson-0.3.1.tar.gz", hash = "sha256:a170883f04a9c5e72a3831ce65720324b078483e0360e6f249400e1c4b8d9ffa", size = 20275, upload-time = "2025-07-11T08:26:13.243Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/d5/4a7974d072c5eb6565e5abe3d52b13f662491934dac4b9ea15836fa2da69/kajson-0.3.0-py3-none-any.whl", hash = "sha256:d8cc72875ebef6ea44c06c96fe63accf5b0721ca24ea1e593814dd45fd25bf15", size = 26711, upload-time = "2025-07-09T09:52:32.639Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/3b/3529c470a4cc1051abe2e4cd6a8a714b14eea26a8486bb3e1cd19a5c60d9/kajson-0.3.1-py3-none-any.whl", hash = "sha256:c129b5dd66f757ed8077c49803ea290f91ba6f5699717eb97b65d960bcc83ac4", size = 26721, upload-time = "2025-07-11T08:26:11.622Z" },
]
[[package]]
@@ -1120,16 +1019,15 @@ wheels = [
[[package]]
name = "my-project"
-version = "0.2.2"
+version = "0.3.0"
source = { virtual = "." }
dependencies = [
- { name = "pipelex", extra = ["anthropic", "bedrock", "fal", "google", "mistralai"] },
+ { name = "pipelex", extra = ["anthropic", "bedrock", "fal", "google", "google-genai", "mistralai"] },
]
[package.optional-dependencies]
dev = [
{ name = "boto3-stubs" },
- { name = "cocode" },
{ name = "mypy" },
{ name = "pyright" },
{ name = "pytest" },
@@ -1138,35 +1036,28 @@ dev = [
{ name = "ruff" },
{ name = "types-aioboto3", extra = ["bedrock", "bedrock-runtime"] },
{ name = "types-aiofiles" },
- { name = "types-beautifulsoup4" },
{ name = "types-markdown" },
{ name = "types-networkx" },
{ name = "types-openpyxl" },
{ name = "types-pyyaml" },
- { name = "types-requests" },
- { name = "types-toml" },
]
[package.metadata]
requires-dist = [
{ name = "boto3-stubs", marker = "extra == 'dev'", specifier = ">=1.35.24" },
- { name = "cocode", marker = "extra == 'dev'", specifier = "==0.1.2" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.2" },
- { name = "pipelex", extras = ["mistralai", "anthropic", "google", "bedrock", "fal"], specifier = "==0.10.2" },
- { name = "pyright", marker = "extra == 'dev'", specifier = "==1.1.398" },
+ { name = "pipelex", extras = ["mistralai", "anthropic", "google", "google-genai", "bedrock", "fal"], git = "https://github.com/Pipelex/pipelex.git" },
+ { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.405" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
{ name = "pytest-sugar", marker = "extra == 'dev'", specifier = ">=1.0.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.8" },
{ name = "types-aioboto3", extras = ["bedrock", "bedrock-runtime"], marker = "extra == 'dev'", specifier = ">=13.4.0" },
{ name = "types-aiofiles", marker = "extra == 'dev'", specifier = ">=24.1.0.20240626" },
- { name = "types-beautifulsoup4", marker = "extra == 'dev'", specifier = ">=4.12.0.20240907" },
{ name = "types-markdown", marker = "extra == 'dev'", specifier = ">=3.6.0.20240316" },
{ name = "types-networkx", marker = "extra == 'dev'", specifier = ">=3.3.0.20241020" },
{ name = "types-openpyxl", marker = "extra == 'dev'", specifier = ">=3.1.5.20250306" },
{ name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250326" },
- { name = "types-requests", marker = "extra == 'dev'", specifier = ">=2.32.0.2024091" },
- { name = "types-toml", marker = "extra == 'dev'", specifier = ">=0.10.8.20240310" },
]
provides-extras = ["dev"]
@@ -1235,7 +1126,9 @@ name = "networkx"
version = "3.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.12'",
+ "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.12' and python_full_version < '3.14' and platform_python_implementation != 'PyPy'",
+ "python_full_version >= '3.12' and platform_python_implementation == 'PyPy'",
"python_full_version == '3.11.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" }
@@ -1342,18 +1235,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/d9/7ec61c010f0d0b0bc57dab8b8dff398f84230d269e8bfa068ad542ff050c/openai-1.82.1-py3-none-any.whl", hash = "sha256:334eb5006edf59aa464c9e932b9d137468d810b2659e5daea9b3a8c39d052395", size = 720466, upload-time = "2025-05-29T16:15:12.531Z" },
]
-[[package]]
-name = "openpyxl"
-version = "3.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "et-xmlfile" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
-]
-
[[package]]
name = "packaging"
version = "25.0"
@@ -1363,54 +1244,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
-[[package]]
-name = "pandas"
-version = "2.2.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
- { name = "python-dateutil" },
- { name = "pytz" },
- { name = "tzdata" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827, upload-time = "2024-09-20T13:08:42.347Z" },
- { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897, upload-time = "2024-09-20T13:08:45.807Z" },
- { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908, upload-time = "2024-09-20T18:37:13.513Z" },
- { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210, upload-time = "2024-09-20T13:08:48.325Z" },
- { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292, upload-time = "2024-09-20T19:01:54.443Z" },
- { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379, upload-time = "2024-09-20T13:08:50.882Z" },
- { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471, upload-time = "2024-09-20T13:08:53.332Z" },
- { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" },
- { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" },
- { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" },
- { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" },
- { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" },
- { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" },
- { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" },
- { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" },
- { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" },
- { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" },
- { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" },
- { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" },
- { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" },
- { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" },
- { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" },
- { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" },
- { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" },
- { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" },
- { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" },
- { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" },
- { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" },
- { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" },
- { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" },
- { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" },
- { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" },
- { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" },
- { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
-]
-
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -1499,8 +1332,8 @@ wheels = [
[[package]]
name = "pipelex"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
+version = "0.12.0"
+source = { git = "https://github.com/Pipelex/pipelex.git#cc0ae6e4b30ff960544c5f2d4728d8b0df52fd46" }
dependencies = [
{ name = "aiofiles" },
{ name = "backports-strenum", marker = "python_full_version < '3.11'" },
@@ -1514,8 +1347,6 @@ dependencies = [
{ name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "openai" },
- { name = "openpyxl" },
- { name = "pandas" },
{ name = "pillow" },
{ name = "polyfactory" },
{ name = "pydantic" },
@@ -1524,16 +1355,12 @@ dependencies = [
{ name = "pyyaml" },
{ name = "rich" },
{ name = "shortuuid" },
- { name = "toml" },
+ { name = "tomli" },
{ name = "tomlkit" },
{ name = "typer" },
{ name = "typing-extensions" },
{ name = "yattag" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c1/a4/bbbf65a7884f90bb4db3bdb05c4c4e781d634f9ca3281526e9601eebea0d/pipelex-0.10.2.tar.gz", hash = "sha256:35fa0c9934574b77d4e622fc22af36c250ce4ef1b0ddbb3a948f65cf60f79edd", size = 219534, upload-time = "2025-09-18T10:37:37.588Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/b3/7529b5780e75a611ac7ef4c15783ff5e888b1e43661893bc8c73b86a146c/pipelex-0.10.2-py3-none-any.whl", hash = "sha256:732a91ca2978c41b120806406b84297532877842ebd8eb4e38c3ecda32c2f668", size = 356816, upload-time = "2025-09-18T10:37:35.672Z" },
-]
[package.optional-dependencies]
anthropic = [
@@ -1549,6 +1376,10 @@ fal = [
google = [
{ name = "google-auth-oauthlib" },
]
+google-genai = [
+ { name = "google-genai" },
+ { name = "instructor", extra = ["google-genai"] },
+]
mistralai = [
{ name = "mistralai" },
]
@@ -1685,15 +1516,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
-[[package]]
-name = "pycparser"
-version = "2.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
-]
-
[[package]]
name = "pydantic"
version = "2.10.6"
@@ -1783,23 +1605,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715, upload-time = "2024-12-18T11:31:22.821Z" },
]
-[[package]]
-name = "pygithub"
-version = "2.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "deprecated" },
- { name = "pyjwt", extra = ["crypto"] },
- { name = "pynacl" },
- { name = "requests" },
- { name = "typing-extensions" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/a0/1e8b8ca88df9857836f5bf8e3ee15dfb810d19814ef700b12f99ce11f691/pygithub-2.4.0.tar.gz", hash = "sha256:6601e22627e87bac192f1e2e39c6e6f69a43152cfb8f307cee575879320b3051", size = 3476673, upload-time = "2024-08-26T06:49:44.029Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/f3/e185613c411757c0c18b904ea2db173f2872397eddf444a3fe8cdde47077/PyGithub-2.4.0-py3-none-any.whl", hash = "sha256:81935aa4bdc939fba98fee1cb47422c09157c56a27966476ff92775602b9ee24", size = 362599, upload-time = "2024-08-26T06:49:42.351Z" },
-]
-
[[package]]
name = "pygments"
version = "2.19.1"
@@ -1809,40 +1614,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
]
-[[package]]
-name = "pyjwt"
-version = "2.10.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
-]
-
-[package.optional-dependencies]
-crypto = [
- { name = "cryptography" },
-]
-
-[[package]]
-name = "pynacl"
-version = "1.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" },
- { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" },
- { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" },
- { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" },
- { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" },
- { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" },
- { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" },
- { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" },
- { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
-]
-
[[package]]
name = "pypdfium2"
version = "4.30.0"
@@ -1865,15 +1636,15 @@ wheels = [
[[package]]
name = "pyright"
-version = "1.1.398"
+version = "1.1.406"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675, upload-time = "2025-03-26T10:06:06.063Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235, upload-time = "2025-03-26T10:06:03.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" },
]
[[package]]
@@ -1940,15 +1711,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
]
-[[package]]
-name = "pytz"
-version = "2025.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
-]
-
[[package]]
name = "pyyaml"
version = "6.0.2"
@@ -2138,52 +1900,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
]
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
-]
-
[[package]]
name = "tomli"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
- { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
- { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
- { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
- { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
- { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
- { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
- { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
- { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
- { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
- { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
- { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
- { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
- { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
- { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
- { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
- { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
- { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
- { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
- { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
- { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
- { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
- { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
- { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
- { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
- { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
- { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
- { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
- { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
- { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
+ { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
+ { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
+ { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
+ { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
+ { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
+ { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
+ { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
+ { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
+ { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
+ { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
+ { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
]
[[package]]
@@ -2300,27 +2063,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/82/1ee2e5c9d28deac086ab3a6ff07c8bc393ef013a083f546c623699881715/types_awscrt-0.27.2-py3-none-any.whl", hash = "sha256:49a045f25bbd5ad2865f314512afced933aed35ddbafc252e2268efa8a787e4e", size = 37761, upload-time = "2025-05-16T03:10:07.466Z" },
]
-[[package]]
-name = "types-beautifulsoup4"
-version = "4.12.0.20250516"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "types-html5lib" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6d/d1/32b410f6d65eda94d3dfb0b3d0ca151f12cb1dc4cef731dcf7cbfd8716ff/types_beautifulsoup4-4.12.0.20250516.tar.gz", hash = "sha256:aa19dd73b33b70d6296adf92da8ab8a0c945c507e6fb7d5db553415cc77b417e", size = 16628, upload-time = "2025-05-16T03:09:09.93Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/79/d84de200a80085b32f12c5820d4fd0addcbe7ba6dce8c1c9d8605e833c8e/types_beautifulsoup4-4.12.0.20250516-py3-none-any.whl", hash = "sha256:5923399d4a1ba9cc8f0096fe334cc732e130269541d66261bb42ab039c0376ee", size = 16879, upload-time = "2025-05-16T03:09:09.051Z" },
-]
-
-[[package]]
-name = "types-html5lib"
-version = "1.1.11.20250516"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/ed/9f092ff479e2b5598941855f314a22953bb04b5fb38bcba3f880feb833ba/types_html5lib-1.1.11.20250516.tar.gz", hash = "sha256:65043a6718c97f7d52567cc0cdf41efbfc33b1f92c6c0c5e19f60a7ec69ae720", size = 16136, upload-time = "2025-05-16T03:07:12.231Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/3b/cb5b23c7b51bf48b8c9f175abb9dce2f1ecd2d2c25f92ea9f4e3720e9398/types_html5lib-1.1.11.20250516-py3-none-any.whl", hash = "sha256:5e407b14b1bd2b9b1107cbd1e2e19d4a0c46d60febd231c7ab7313d7405663c1", size = 21770, upload-time = "2025-05-16T03:07:11.102Z" },
-]
-
[[package]]
name = "types-markdown"
version = "3.8.0.20250415"
@@ -2360,18 +2102,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" },
]
-[[package]]
-name = "types-requests"
-version = "2.32.0.20250515"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/c1/cdc4f9b8cfd9130fbe6276db574f114541f4231fcc6fb29648289e6e3390/types_requests-2.32.0.20250515.tar.gz", hash = "sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581", size = 23012, upload-time = "2025-05-15T03:04:31.817Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/0f/68a997c73a129287785f418c1ebb6004f81e46b53b3caba88c0e03fcd04a/types_requests-2.32.0.20250515-py3-none-any.whl", hash = "sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2", size = 20635, upload-time = "2025-05-15T03:04:30.5Z" },
-]
-
[[package]]
name = "types-s3transfer"
version = "0.13.0"
@@ -2381,15 +2111,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/5d/6bbe4bf6a79fb727945291aef88b5ecbdba857a603f1bbcf1a6be0d3f442/types_s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:79c8375cbf48a64bff7654c02df1ec4b20d74f8c5672fc13e382f593ca5565b3", size = 19588, upload-time = "2025-05-28T02:16:06.709Z" },
]
-[[package]]
-name = "types-toml"
-version = "0.10.8.20240310"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392, upload-time = "2024-03-10T02:18:37.518Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777, upload-time = "2024-03-10T02:18:36.568Z" },
-]
-
[[package]]
name = "typing-extensions"
version = "4.13.2"
@@ -2430,6 +2151,65 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
+ { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
[[package]]
name = "wrapt"
version = "1.17.2"