From 4b948a54efda9e8eca6a86982cc41eab44d52373 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 20:21:05 +0700 Subject: [PATCH 01/42] feat: add Jinja2 template edge processor Implement template edge processor for transforming edge payloads using Jinja2 templates. Changes: - Add jinja2>=3.1.0 dependency to pyproject.toml - Create TemplateEdgeProcessorConfig in entity/configs/edge/edge_processor.py - Implement TemplateEdgePayloadProcessor with sandboxed Jinja2 environment - Register 'template' processor type in builtin_types registry - Add comprehensive test suite (17 tests, 100% pass rate) - Include template processor example in yaml_template/ Features: - Variable interpolation: input, environment, extracted - Custom filters: fromjson, tojson - Full Jinja2 control structures (if/for/set) - Safe execution with sandboxed environment - Strict undefined variable checking - Descriptive error messages This enables users to separate data formatting logic from LLM prompts, improving maintainability and reducing hallucination in complex workflows like the hospital simulation report generation. Fixes 14 existing template processor edges in simulation_hospital.yaml that were previously failing silently. --- entity/configs/edge/edge_processor.py | 85 +++++-- .../add-template-edge-processor/proposal.md | 42 ++++ .../specs/edge-processors/spec.md | 110 +++++++++ .../add-template-edge-processor/tasks.md | 36 +++ pyproject.toml | 1 + runtime/edge/processors/builtin_types.py | 9 + runtime/edge/processors/template_processor.py | 99 ++++++++ tests/test_template_processor.py | 225 ++++++++++++++++++ uv.lock | 33 +++ yaml_template/template_processor_example.yaml | 86 +++++++ 10 files changed, 711 insertions(+), 15 deletions(-) create mode 100644 openspec/changes/add-template-edge-processor/proposal.md create mode 100644 openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md create mode 100644 openspec/changes/add-template-edge-processor/tasks.md create mode 100644 runtime/edge/processors/template_processor.py create mode 100644 tests/test_template_processor.py create mode 100644 yaml_template/template_processor_example.yaml diff --git a/entity/configs/edge/edge_processor.py b/entity/configs/edge/edge_processor.py index 46566bf99..775c5f347 100755 --- a/entity/configs/edge/edge_processor.py +++ b/entity/configs/edge/edge_processor.py @@ -44,8 +44,6 @@ def to_external_value(self) -> Any: return _serialize_config(self) - - _NO_MATCH_DESCRIPTIONS = { "pass": "Leave the payload untouched when no match is found.", "default": "Apply default_value (or empty string) if nothing matches.", @@ -117,7 +115,6 @@ class RegexEdgeProcessorConfig(EdgeProcessorTypeConfig): description="Whether to collect all matches instead of only the first.", advance=True, ), - "template": ConfigFieldSpec( name="template", display_name="Output Template", @@ -152,7 +149,9 @@ class RegexEdgeProcessorConfig(EdgeProcessorTypeConfig): } @classmethod - def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "RegexEdgeProcessorConfig": + def from_dict( + cls, data: Mapping[str, Any], *, path: str + ) -> "RegexEdgeProcessorConfig": mapping = require_mapping(data, path) pattern = require_str(mapping, "pattern", path, allow_empty=False) group_value = mapping.get("group") @@ -166,14 +165,19 @@ def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "RegexEdgeProcessor else: group_normalized = group_value else: - raise ConfigError("group must be str or int", extend_path(path, "group")) + raise ConfigError( + "group must be str or int", extend_path(path, "group") + ) multiple = optional_bool(mapping, "multiple", path, default=False) case_sensitive = optional_bool(mapping, "case_sensitive", path, default=True) multiline = optional_bool(mapping, "multiline", path, default=False) dotall = optional_bool(mapping, "dotall", path, default=False) on_no_match = optional_str(mapping, "on_no_match", path) or "pass" if on_no_match not in {"pass", "default", "drop"}: - raise ConfigError("on_no_match must be pass, default or drop", extend_path(path, "on_no_match")) + raise ConfigError( + "on_no_match must be pass, default or drop", + extend_path(path, "on_no_match"), + ) template = optional_str(mapping, "template", path) default_value = optional_str(mapping, "default_value", path) @@ -230,18 +234,26 @@ def field_specs(cls) -> Dict[str, ConfigFieldSpec]: descriptions = {} for name in names: meta = metadata.get(name) - descriptions[name] = (meta.description if meta else None) or "No description provided." + descriptions[name] = ( + meta.description if meta else None + ) or "No description provided." specs["name"] = replace( name_spec, enum=names or None, - enum_options=enum_options_from_values(names, descriptions, preserve_label_case=True) if names else None, + enum_options=enum_options_from_values( + names, descriptions, preserve_label_case=True + ) + if names + else None, description=description, ) return specs @classmethod - def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "FunctionEdgeProcessorConfig": + def from_dict( + cls, data: Mapping[str, Any], *, path: str + ) -> "FunctionEdgeProcessorConfig": mapping = require_mapping(data, path) name = require_str(mapping, "name", path, allow_empty=False) return cls(name=name, path=path) @@ -253,6 +265,37 @@ def to_external_value(self) -> Any: return {"name": self.name} +@dataclass +class TemplateEdgeProcessorConfig(EdgeProcessorTypeConfig): + """Configuration for Jinja2 template-based payload processors.""" + + template: str = "" + + FIELD_SPECS = { + "template": ConfigFieldSpec( + name="template", + display_name="Template String", + type_hint="str", + required=True, + description="Jinja2 template string for transforming edge payloads.", + ) + } + + @classmethod + def from_dict( + cls, data: Mapping[str, Any], *, path: str + ) -> "TemplateEdgeProcessorConfig": + mapping = require_mapping(data, path) + template = require_str(mapping, "template", path, allow_empty=False) + return cls(template=template, path=path) + + def display_label(self) -> str: + return "template" + + def to_external_value(self) -> Any: + return {"template": self.template} + + TProcessorConfig = TypeVar("TProcessorConfig", bound=EdgeProcessorTypeConfig) @@ -288,12 +331,18 @@ def from_dict(cls, data: Any, *, path: str) -> "EdgeProcessorConfig": processor_type = require_str(mapping, "type", path) config_payload = mapping.get("config") if config_payload is None: - raise ConfigError("processor config is required", extend_path(path, "config")) + raise ConfigError( + "processor config is required", extend_path(path, "config") + ) try: schema = get_edge_processor_schema(processor_type) except SchemaLookupError as exc: - raise ConfigError(f"unknown processor type '{processor_type}'", extend_path(path, "type")) from exc - processor_config = schema.config_cls.from_dict(config_payload, path=extend_path(path, "config")) + raise ConfigError( + f"unknown processor type '{processor_type}'", extend_path(path, "type") + ) from exc + processor_config = schema.config_cls.from_dict( + config_payload, path=extend_path(path, "config") + ) return cls(type=processor_type, config=processor_config, path=path) @classmethod @@ -310,11 +359,15 @@ def field_specs(cls) -> Dict[str, ConfigFieldSpec]: if type_spec: registrations = iter_edge_processor_schemas() names = list(registrations.keys()) - descriptions = {name: schema.summary for name, schema in registrations.items()} + descriptions = { + name: schema.summary for name, schema in registrations.items() + } specs["type"] = replace( type_spec, enum=names, - enum_options=enum_options_from_values(names, descriptions, preserve_label_case=True), + enum_options=enum_options_from_values( + names, descriptions, preserve_label_case=True + ), ) return specs @@ -327,7 +380,9 @@ def to_external_value(self) -> Any: "config": self.config.to_external_value(), } - def as_config(self, expected_type: Type[TProcessorConfig]) -> TProcessorConfig | None: + def as_config( + self, expected_type: Type[TProcessorConfig] + ) -> TProcessorConfig | None: config = self.config if isinstance(config, expected_type): return cast(TProcessorConfig, config) diff --git a/openspec/changes/add-template-edge-processor/proposal.md b/openspec/changes/add-template-edge-processor/proposal.md new file mode 100644 index 000000000..8f7c0ae29 --- /dev/null +++ b/openspec/changes/add-template-edge-processor/proposal.md @@ -0,0 +1,42 @@ +# Change: Add Template Edge Processor + +## Why + +The workflow uses `type: template` in edge processors (e.g., `yaml_instance/simulation_hospital.yaml` lines 960, 1006, 1036), but **no template processor implementation exists**. This causes processor resolution failures or silent falls back to regex/function processors. + +Users want to: +1. Use Jinja2 templates for edge payload transformation (data formatting, conditional logic) +2. Separate report templates from LLM prompts (FinalReportAggregator currently has a 50+ line prompt embedding the report structure) +3. Access edge context variables (`input`, `environment.output`, etc.) in templates + +Currently, users must embed all formatting logic in LLM prompts or use regex processors, which is verbose and error-prone. + +## What Changes + +- **ADD** `TemplateEdgeProcessorConfig` dataclass in `entity/configs/edge/edge_processor.py` +- **ADD** `TemplateEdgePayloadProcessor` class in `runtime/edge/processors/template_processor.py` +- **REGISTER** `template` processor type in `runtime/edge/processors/builtin_types.py` +- **ADD** Jinja2 dependency to `pyproject.toml` +- **ADD** unit tests in `tests/test_template_processor.py` + +The processor will: +- Accept `template: str` (Jinja2 template string) +- Provide context variables: `input` (payload), `environment` (global state), `extracted` (from prior processors) +- Support Jinja2 filters: `fromjson`, `tojson`, `default`, standard filters +- Return transformed string payload + +## Impact + +**Affected specs:** +- `edge-processors` (new spec) + +**Affected code:** +- `entity/configs/edge/edge_processor.py` - Add config class +- `runtime/edge/processors/template_processor.py` - New processor implementation +- `runtime/edge/processors/builtin_types.py` - Register processor +- `pyproject.toml` - Add Jinja2 dependency +- `yaml_instance/simulation_hospital.yaml` - Already uses `type: template` (will now work correctly) + +**Breaking changes:** None (adds new functionality) + +**Migration:** Existing workflows using `type: template` will now work correctly instead of failing silently. diff --git a/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md b/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md new file mode 100644 index 000000000..5b25a607f --- /dev/null +++ b/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md @@ -0,0 +1,110 @@ +## ADDED Requirements + +### Requirement: Template Processor Type Registration + +The system SHALL register a `template` edge processor type that transforms edge payloads using Jinja2 template rendering. + +#### Scenario: Template processor is available +- **WHEN** workflow validation runs or edge processor registry is queried +- **THEN** `template` appears in the list of available processor types +- **AND** processor summary is "Transform payloads using Jinja2 templates" + +### Requirement: Template Configuration + +The template processor SHALL accept a `template` configuration field containing a Jinja2 template string. + +#### Scenario: Valid template configuration +- **WHEN** edge config contains `processor: {type: template, config: {template: "Hello {{ input }}"}}` +- **THEN** configuration parses successfully +- **AND** `TemplateEdgeProcessorConfig` instance is created with template field set + +#### Scenario: Missing template field +- **WHEN** edge config contains `processor: {type: template, config: {}}` +- **THEN** configuration parsing raises `ConfigError` +- **AND** error message indicates `template` field is required + +#### Scenario: Empty template string +- **WHEN** edge config contains `processor: {type: template, config: {template: ""}}` +- **THEN** configuration parsing raises `ConfigError` +- **AND** error message indicates template cannot be empty + +### Requirement: Template Context Variables + +The template processor SHALL provide context variables for use in Jinja2 templates. + +#### Scenario: Input variable access +- **WHEN** template is `"Input: {{ input }}"` +- **AND** edge payload is `"test message"` +- **THEN** processor returns `"Input: test message"` + +#### Scenario: Environment variable access +- **WHEN** template is `"Env: {{ environment.output }}"` +- **AND** environment node output is `"env data"` +- **THEN** processor returns `"Env: env data"` + +#### Scenario: Extracted variable access (from prior processors) +- **WHEN** prior regex processor extracted value `"extracted_value"` +- **AND** template is `"Result: {{ extracted }}"` +- **THEN** processor returns `"Result: extracted_value"` + +### Requirement: Jinja2 Filters + +The template processor SHALL support standard Jinja2 filters plus custom `fromjson` and `tojson` filters. + +#### Scenario: JSON parsing with fromjson filter +- **WHEN** template is `"{% set data = input | fromjson %}Name: {{ data.name }}"` +- **AND** input payload is `'{"name": "Alice"}'` +- **THEN** processor returns `"Name: Alice"` + +#### Scenario: JSON serialization with tojson filter +- **WHEN** template is `"{{ {\"key\": \"value\"} | tojson }}"` +- **THEN** processor returns `"{\"key\": \"value\"}"` + +#### Scenario: Default filter for missing variables +- **WHEN** template is `"Value: {{ missing | default('N/A') }}"` +- **AND** variable `missing` is not in context +- **THEN** processor returns `"Value: N/A"` + +### Requirement: Template Conditional Logic + +The template processor SHALL support Jinja2 control structures (if/for/set). + +#### Scenario: Conditional rendering +- **WHEN** template is `"{% if input == 'yes' %}Confirmed{% else %}Denied{% endif %}"` +- **AND** input is `"yes"` +- **THEN** processor returns `"Confirmed"` + +#### Scenario: Variable assignment +- **WHEN** template is `"{% set x = input | fromjson %}Result: {{ x.field }}"` +- **AND** input is `'{"field": "data"}'` +- **THEN** processor returns `"Result: data"` + +### Requirement: Error Handling + +The template processor SHALL handle template rendering errors gracefully with descriptive messages. + +#### Scenario: Invalid template syntax +- **WHEN** template contains `"{{ unclosed variable"` +- **THEN** processor raises `TemplateRenderError` during initialization +- **AND** error message includes "Invalid template syntax" + +#### Scenario: Undefined variable (strict mode) +- **WHEN** template is `"{{ undefined_var }}"` +- **AND** variable is not in context +- **THEN** processor raises `TemplateRenderError` +- **AND** error message includes variable name and available context keys + +#### Scenario: JSON parsing error +- **WHEN** template uses `"{{ input | fromjson }}"` +- **AND** input is invalid JSON +- **THEN** processor raises `TemplateRenderError` +- **AND** error message includes "JSON decode error" + +### Requirement: Safe Template Execution + +The template processor SHALL use Jinja2 sandboxing to prevent arbitrary code execution. + +#### Scenario: Restricted environment +- **WHEN** template processor initializes Jinja2 environment +- **THEN** environment uses `jinja2.sandbox.SandboxedEnvironment` +- **AND** unsafe operations (file access, imports) are blocked diff --git a/openspec/changes/add-template-edge-processor/tasks.md b/openspec/changes/add-template-edge-processor/tasks.md new file mode 100644 index 000000000..feaaa39c5 --- /dev/null +++ b/openspec/changes/add-template-edge-processor/tasks.md @@ -0,0 +1,36 @@ +## 1. Implementation + +- [x] 1.1 Add Jinja2 dependency to `pyproject.toml` (`jinja2>=3.1.0`) +- [x] 1.2 Create `TemplateEdgeProcessorConfig` in `entity/configs/edge/edge_processor.py` + - Fields: `template: str` + - Validation: template cannot be empty +- [x] 1.3 Create `TemplateEdgePayloadProcessor` in `runtime/edge/processors/template_processor.py` + - Initialize Jinja2 environment with safe mode + - Register custom filters: `fromjson`, `tojson` + - Implement `process(input, context)` method + - Provide context: `input`, `environment`, `extracted` +- [x] 1.4 Register processor in `runtime/edge/processors/builtin_types.py` + - Type name: `"template"` + - Summary: "Transform payloads using Jinja2 templates" + +## 2. Testing + +- [x] 2.1 Create `tests/test_template_processor.py` + - Test basic variable substitution (`{{ input }}`) + - Test JSON parsing (`{% set data = input | fromjson %}`) + - Test conditional logic (`{% if ... %}`) + - Test environment variable access (`{{ environment.output }}`) + - Test error handling (invalid template, missing variables) +- [x] 2.2 Run validation: `uv run python -m check.check --path yaml_instance/simulation_hospital.yaml` +- [x] 2.3 Run integration test: `uv run pytest tests/test_template_processor.py -v` + +## 3. Documentation + +- [x] 3.1 Add template processor example to `yaml_template/` directory +- [x] 3.2 Update user guide: `docs/user_guide/en/modules/edge_processors.md` (if exists) - N/A: docs don't exist yet + +## 4. Validation + +- [x] 4.1 Verify existing `simulation_hospital.yaml` edges with `type: template` now work +- [x] 4.2 Run full test suite: `uv run pytest` +- [x] 4.3 Validate change: `openspec validate add-template-edge-processor --strict --no-interactive` diff --git a/pyproject.toml b/pyproject.toml index e1dad7064..834f05313 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "filelock>=3.20.1", "markdown>=3.10", "xhtml2pdf>=0.2.17", + "jinja2>=3.1.0", ] [build-system] diff --git a/runtime/edge/processors/builtin_types.py b/runtime/edge/processors/builtin_types.py index 96a50ecd9..0caf4fe25 100755 --- a/runtime/edge/processors/builtin_types.py +++ b/runtime/edge/processors/builtin_types.py @@ -3,10 +3,12 @@ from entity.configs.edge.edge_processor import ( RegexEdgeProcessorConfig, FunctionEdgeProcessorConfig, + TemplateEdgeProcessorConfig, ) from .registry import register_edge_processor from .regex_processor import RegexEdgePayloadProcessor from .function_processor import FunctionEdgePayloadProcessor +from .template_processor import TemplateEdgePayloadProcessor register_edge_processor( "regex_extract", @@ -21,3 +23,10 @@ summary="Delegate message transformation to Python functions in functions/edge_processor.", config_cls=FunctionEdgeProcessorConfig, ) + +register_edge_processor( + "template", + processor_cls=TemplateEdgePayloadProcessor, + summary="Transform payloads using Jinja2 templates.", + config_cls=TemplateEdgeProcessorConfig, +) diff --git a/runtime/edge/processors/template_processor.py b/runtime/edge/processors/template_processor.py new file mode 100644 index 000000000..1addc3b2a --- /dev/null +++ b/runtime/edge/processors/template_processor.py @@ -0,0 +1,99 @@ +"""Jinja2 template-based edge payload processor.""" + +import json +from typing import Any + +from jinja2 import Environment, TemplateSyntaxError, UndefinedError, StrictUndefined +from jinja2.sandbox import SandboxedEnvironment + +from entity.configs.edge.edge_processor import TemplateEdgeProcessorConfig +from entity.messages import Message +from runtime.node.executor import ExecutionContext +from utils.log_manager import LogManager + +from .base import EdgePayloadProcessor, ProcessorFactoryContext + + +class TemplateRenderError(Exception): + """Raised when template rendering fails.""" + + pass + + +def _fromjson_filter(value: str) -> Any: + """Parse JSON string into Python object.""" + try: + return json.loads(value) + except json.JSONDecodeError as exc: + raise TemplateRenderError(f"JSON decode error: {exc}") from exc + + +def _tojson_filter(value: Any) -> str: + """Serialize Python object to JSON string.""" + try: + return json.dumps(value, ensure_ascii=False) + except (TypeError, ValueError) as exc: + raise TemplateRenderError(f"JSON encode error: {exc}") from exc + + +class TemplateEdgePayloadProcessor(EdgePayloadProcessor[TemplateEdgeProcessorConfig]): + """Transform edge payloads using Jinja2 templates.""" + + def __init__( + self, config: TemplateEdgeProcessorConfig, ctx: ProcessorFactoryContext + ) -> None: + super().__init__(config, ctx) + + # Create sandboxed Jinja2 environment + self.env = SandboxedEnvironment( + autoescape=False, + undefined=StrictUndefined, # Strict mode - fail on undefined variables + ) + + # Register custom filters + self.env.filters["fromjson"] = _fromjson_filter + self.env.filters["tojson"] = _tojson_filter + + # Compile template during initialization to catch syntax errors early + try: + self.template = self.env.from_string(config.template) + except TemplateSyntaxError as exc: + raise TemplateRenderError(f"Invalid template syntax: {exc}") from exc + + def transform( + self, + payload: Message, + *, + source_result: Message, + from_node: Any, + edge_link: Any, + log_manager: LogManager, + context: ExecutionContext, + ) -> Message | None: + """Render template with payload and context variables.""" + input_text = self._text(payload) + + # Build template context + template_context = { + "input": input_text, + "environment": getattr(context, "environment", {}), + "extracted": input_text, # Default to input if no prior extraction + } + + # Render template + try: + output = self.template.render(template_context) + except UndefinedError as exc: + available_keys = ", ".join(sorted(template_context.keys())) + raise TemplateRenderError( + f"Undefined variable in template: {exc}. Available context keys: {available_keys}" + ) from exc + except TemplateRenderError: + raise + except Exception as exc: + raise TemplateRenderError(f"Template rendering failed: {exc}") from exc + + # Return new message with rendered output + cloned = payload.clone() + cloned.content = output + return cloned diff --git a/tests/test_template_processor.py b/tests/test_template_processor.py new file mode 100644 index 000000000..609495cea --- /dev/null +++ b/tests/test_template_processor.py @@ -0,0 +1,225 @@ +"""Tests for template edge processor.""" + +import pytest + +from entity.configs.edge.edge_processor import TemplateEdgeProcessorConfig +from entity.configs.base import ConfigError +from entity.messages import Message, MessageRole +from runtime.edge.processors.template_processor import ( + TemplateEdgePayloadProcessor, + TemplateRenderError, +) +from runtime.edge.processors.base import ProcessorFactoryContext +from runtime.node.executor import ExecutionContext +from utils.log_manager import LogManager + + +class MockExecutionContext: + """Mock execution context for testing.""" + + def __init__(self, environment=None): + self.environment = environment or {} + + +def create_processor(template: str) -> TemplateEdgePayloadProcessor: + """Helper to create processor with template.""" + config = TemplateEdgeProcessorConfig(template=template, path="test") + ctx = ProcessorFactoryContext() + return TemplateEdgePayloadProcessor(config, ctx) + + +def transform_text( + processor: TemplateEdgePayloadProcessor, input_text: str, environment=None +) -> str: + """Helper to transform text input.""" + payload = Message(role=MessageRole.USER, content=input_text) + context = MockExecutionContext(environment=environment) + result = processor.transform( + payload, + source_result=payload, + from_node=None, + edge_link=None, + log_manager=LogManager(), + context=context, # type: ignore + ) + return result.text_content() if result else "" + + +class TestTemplateConfiguration: + """Test template configuration validation.""" + + def test_valid_template_configuration(self): + """Valid template configuration should parse successfully.""" + config = TemplateEdgeProcessorConfig(template="Hello {{ input }}", path="test") + assert config.template == "Hello {{ input }}" + + def test_missing_template_field(self): + """Missing template field should raise ConfigError.""" + with pytest.raises(ConfigError, match="expected string"): + TemplateEdgeProcessorConfig.from_dict({}, path="test") + + def test_empty_template_string(self): + """Empty template string should raise ConfigError.""" + with pytest.raises(ConfigError, match="expected non-empty string"): + TemplateEdgeProcessorConfig.from_dict({"template": ""}, path="test") + + +class TestBasicVariableSubstitution: + """Test basic template variable substitution.""" + + def test_input_variable_access(self): + """Template should access input variable.""" + processor = create_processor("Input: {{ input }}") + result = transform_text(processor, "test message") + assert result == "Input: test message" + + def test_environment_variable_access(self): + """Template should access environment variables.""" + processor = create_processor("Env: {{ environment.output }}") + result = transform_text( + processor, "ignored", environment={"output": "env data"} + ) + assert result == "Env: env data" + + def test_extracted_variable_access(self): + """Template should access extracted variable (defaults to input).""" + processor = create_processor("Result: {{ extracted }}") + result = transform_text(processor, "extracted_value") + assert result == "Result: extracted_value" + + +class TestJinja2Filters: + """Test Jinja2 filter support.""" + + def test_fromjson_filter(self): + """Template should parse JSON with fromjson filter.""" + processor = create_processor( + "{% set data = input | fromjson %}Name: {{ data.name }}" + ) + result = transform_text(processor, '{"name": "Alice"}') + assert result == "Name: Alice" + + def test_tojson_filter(self): + """Template should serialize to JSON with tojson filter.""" + processor = create_processor('{{ {"key": "value"} | tojson }}') + result = transform_text(processor, "ignored") + assert result == '{"key": "value"}' + + def test_default_filter_for_missing_variables(self): + """Template should use default value for missing variables.""" + processor = create_processor("Value: {{ missing | default('N/A') }}") + result = transform_text(processor, "ignored") + assert result == "Value: N/A" + + def test_fromjson_invalid_json(self): + """fromjson filter should raise error on invalid JSON.""" + processor = create_processor("{{ input | fromjson }}") + with pytest.raises(TemplateRenderError, match="JSON decode error"): + transform_text(processor, "not valid json") + + +class TestConditionalLogic: + """Test Jinja2 control structures.""" + + def test_conditional_rendering(self): + """Template should support if/else conditionals.""" + processor = create_processor( + "{% if input == 'yes' %}Confirmed{% else %}Denied{% endif %}" + ) + assert transform_text(processor, "yes") == "Confirmed" + assert transform_text(processor, "no") == "Denied" + + def test_variable_assignment(self): + """Template should support variable assignment with set.""" + processor = create_processor( + "{% set x = input | fromjson %}Result: {{ x.field }}" + ) + result = transform_text(processor, '{"field": "data"}') + assert result == "Result: data" + + def test_for_loop(self): + """Template should support for loops.""" + processor = create_processor( + "{% set items = input | fromjson %}{% for item in items %}{{ item }}{% if not loop.last %},{% endif %}{% endfor %}" + ) + result = transform_text(processor, '["a", "b", "c"]') + assert result == "a,b,c" + + +class TestErrorHandling: + """Test template error handling.""" + + def test_invalid_template_syntax(self): + """Invalid template syntax should raise error during initialization.""" + config = TemplateEdgeProcessorConfig( + template="{{ unclosed variable", path="test" + ) + ctx = ProcessorFactoryContext() + with pytest.raises(TemplateRenderError, match="Invalid template syntax"): + TemplateEdgePayloadProcessor(config, ctx) + + def test_undefined_variable_strict_mode(self): + """Undefined variable should raise error with available context keys.""" + processor = create_processor("{{ undefined_var }}") + with pytest.raises( + TemplateRenderError, match="Undefined variable.*Available context keys" + ): + transform_text(processor, "test") + + +class TestComplexScenarios: + """Test complex real-world template scenarios.""" + + def test_medical_report_template(self): + """Template should handle complex medical report formatting.""" + template = """{% set env = input | fromjson %} +# Medical Report: {{ env.outbreak }} + +Total Patients: {{ env.total_patients }} + +{% for patient in env.patients %} +## Patient {{ loop.index }}: {{ patient.name }} +- Diagnosis: {{ patient.diagnosis | default('Pending') }} +{% endfor %}""" + + processor = create_processor(template) + input_data = { + "outbreak": "COVID-19", + "total_patients": 2, + "patients": [{"name": "Alice", "diagnosis": "Influenza"}, {"name": "Bob"}], + } + + import json + + result = transform_text(processor, json.dumps(input_data)) + + assert "# Medical Report: COVID-19" in result + assert "Total Patients: 2" in result + assert "## Patient 1: Alice" in result + assert "- Diagnosis: Influenza" in result + assert "## Patient 2: Bob" in result + assert "- Diagnosis: Pending" in result + + def test_environment_context_formatting(self): + """Template should format data from environment context.""" + template = """{% set env = environment.output | fromjson %} +OUTBREAK: {{ env.outbreak }} +URGENCY: {{ env.urgency_level }} +CONDITIONS: {{ env.atmospheric_description }}""" + + processor = create_processor(template) + env_data = { + "outbreak": "COVID-19 pandemic", + "urgency_level": "High", + "atmospheric_description": "Hospital overflowing", + } + + import json + + result = transform_text( + processor, "ignored", environment={"output": json.dumps(env_data)} + ) + + assert "OUTBREAK: COVID-19 pandemic" in result + assert "URGENCY: High" in result + assert "CONDITIONS: Hospital overflowing" in result diff --git a/uv.lock b/uv.lock index d79bc2d97..b09d58d69 100755 --- a/uv.lock +++ b/uv.lock @@ -384,6 +384,7 @@ dependencies = [ { name = "fastmcp" }, { name = "filelock" }, { name = "google-genai" }, + { name = "jinja2" }, { name = "markdown" }, { name = "matplotlib" }, { name = "mcp" }, @@ -417,6 +418,7 @@ requires-dist = [ { name = "fastmcp" }, { name = "filelock", specifier = ">=3.20.1" }, { name = "google-genai", specifier = ">=1.52.0" }, + { name = "jinja2", specifier = ">=3.1.0" }, { name = "markdown", specifier = ">=3.10" }, { name = "matplotlib" }, { name = "mcp" }, @@ -859,6 +861,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jiter" version = "0.12.0" @@ -1030,6 +1044,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + [[package]] name = "matplotlib" version = "3.10.8" diff --git a/yaml_template/template_processor_example.yaml b/yaml_template/template_processor_example.yaml new file mode 100644 index 000000000..212883821 --- /dev/null +++ b/yaml_template/template_processor_example.yaml @@ -0,0 +1,86 @@ +# Template Edge Processor Example +# +# This workflow demonstrates the template edge processor for transforming +# edge payloads using Jinja2 templates. + +vars: + API_KEY: ${API_KEY} + BASE_URL: ${BASE_URL} + +nodes: + - id: DataSource + type: agent + name: Data Generator + description: Generates sample JSON data + config: + role: | + Generate a JSON object with patient information. + Output format: + { + "name": "John Doe", + "age": 45, + "diagnosis": "Influenza", + "severity": "Medium" + } + provider: openai + name: gpt-4 + api_key: ${API_KEY} + base_url: ${BASE_URL} + + - id: TemplateFormatter + type: agent + name: Report Formatter + description: Formats the report using the template + config: + role: | + You received formatted patient data. Simply output it as-is. + provider: openai + name: gpt-4 + api_key: ${API_KEY} + base_url: ${BASE_URL} + +edges: + # Example 1: Basic variable substitution + - from: DataSource + to: TemplateFormatter + processor: + type: template + config: + template: | + # Patient Report + + **Patient Name:** {{ (input | fromjson).name }} + **Age:** {{ (input | fromjson).age }} + **Diagnosis:** {{ (input | fromjson).diagnosis }} + **Severity:** {{ (input | fromjson).severity }} + + {% set data = input | fromjson %} + {% if data.severity == "High" %} + ⚠️ URGENT: This case requires immediate attention. + {% elif data.severity == "Medium" %} + ℹ️ This case should be reviewed within 24 hours. + {% else %} + ✓ This case is low priority. + {% endif %} + +# Template Features Demonstrated: +# +# 1. JSON Parsing with fromjson filter: +# {% set data = input | fromjson %} +# +# 2. Variable Access: +# {{ data.name }} +# +# 3. Conditional Logic: +# {% if data.severity == "High" %}...{% endif %} +# +# 4. Default Values: +# {{ data.field | default('N/A') }} +# +# 5. For Loops: +# {% for item in data.items %}{{ item }}{% endfor %} +# +# Available Context Variables: +# - input: The edge payload content +# - environment: Global environment state +# - extracted: Value from prior processor (regex, etc.) From f6cda3c4a2db4f998efbd89fc101c29156e78dd5 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 22:11:50 +0700 Subject: [PATCH 02/42] chores: refactor --- .github/workflows/validate-yamls.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-yamls.yml b/.github/workflows/validate-yamls.yml index f04d802eb..7967c11e8 100644 --- a/.github/workflows/validate-yamls.yml +++ b/.github/workflows/validate-yamls.yml @@ -20,7 +20,8 @@ on: workflow_dispatch: jobs: - validate-yamls: + validate: + name: Validate YAML Configuration Files runs-on: ubuntu-latest steps: @@ -32,7 +33,12 @@ jobs: with: python-version: '3.12' - - name: Install system dependencies + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install system dependencies for pycairo run: | sudo apt-get update sudo apt-get install -y libcairo2-dev pkg-config From b17d6871db6108b2568ab4a2a9cbee5cc9d3dc7b Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 22:13:04 +0700 Subject: [PATCH 03/42] feat: template demo --- yaml_instance/template_demo.yaml | 265 +++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 yaml_instance/template_demo.yaml diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml new file mode 100644 index 000000000..65899b3f0 --- /dev/null +++ b/yaml_instance/template_demo.yaml @@ -0,0 +1,265 @@ +graph: + id: template_demo + description: Template edge processor demonstration with practical examples + log_level: INFO + is_majority_voting: false + nodes: + - id: PatientDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient data. Output ONLY valid JSON: + { + "patient_name": "John Smith", + "age": 68, + "condition": "Influenza", + "severity": "high", + "admission_date": "2024-01-15" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient admission data + context_window: 0 + log_output: true + name: Patient Data Generator + - id: AdmissionReport + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: You received a patient admission report. Acknowledge it. + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: '' + context_window: 0 + log_output: true + name: Admission Report Handler + - id: DailySummary + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: You received multiple reports. Create a brief daily summary. + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: '' + context_window: 0 + log_output: true + name: Daily Summary Generator + - id: TriageReport + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: You received a triage status report. Acknowledge it. + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: '' + context_window: 0 + log_output: true + name: Triage Report Handler + - id: StatusReport + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: You received a status update. Acknowledge it. + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: '' + context_window: 0 + log_output: true + name: Status Report Handler + - id: TriageDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON array of 3 patients in triage: + [ + {"name": "Alice Johnson", "priority": "high", "wait_time": 5}, + {"name": "Bob Williams", "priority": "medium", "wait_time": 20}, + {"name": "Carol Davis", "priority": "low", "wait_time": 45} + ] + Output ONLY valid JSON. + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates triage queue data + context_window: 0 + log_output: true + name: Triage Data Generator + - id: StatusGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: 'Output exactly: "Hospital operations normal"' + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: '' + context_window: 0 + log_output: true + name: Status Generator + edges: + - from: PatientDataGenerator + to: AdmissionReport + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: + type: template + config: + template: | + ============================================== + PATIENT ADMISSION REPORT + ============================================== + {% set patient = input | fromjson %} + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + - Condition: {{ patient.condition }} + - Admission Date: {{ patient.admission_date }} + + Severity Assessment: + {% if patient.severity == "high" %} + URGENT: Immediate attention required + Priority: CRITICAL + {% elif patient.severity == "medium" %} + NOTICE: Review within 24 hours + Priority: MODERATE + {% else %} + ROUTINE: Standard processing + Priority: LOW + {% endif %} + + Case Summary: + {{ patient.patient_name }} ({{ patient.age }}y) admitted with {{ patient.condition }}. + {% if patient.age > 60 %} + Note: Elderly patient - additional monitoring recommended. + {% endif %} + ============================================== + - from: TriageDataGenerator + to: TriageReport + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: + type: template + config: + template: | + ============================================== + TRIAGE QUEUE STATUS + ============================================== + {% set patients = input | fromjson %} + + Current Queue ({{ patients | length }} patients): + + {% for patient in patients %} + {{ loop.index }}. {{ patient.name }} + Priority: {{ patient.priority | upper }} + Wait Time: {{ patient.wait_time }} min + {% if patient.priority == 'high' %} + → ALERT: Fast-track this patient + {% elif patient.wait_time > 30 %} + → WARNING: Extended wait time + {% endif %} + {% endfor %} + ============================================== + - from: StatusGenerator + to: StatusReport + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: + type: template + config: + template: | + System Status: {{ input }} + Emergency Level: {{ emergency_level | default('None') }} + Timestamp: {{ timestamp | default('Not specified') }} + - from: AdmissionReport + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + - from: TriageReport + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + - from: StatusReport + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + memory: [] + initial_instruction: '' + start: + - PatientDataGenerator + - TriageDataGenerator + - StatusGenerator + end: + - DailySummary +version: 0.0.0 +vars: + MODEL_NAME: qwen/qwen3-8b From ea5518923535ebc3d7ac2d418654951731b767ef Mon Sep 17 00:00:00 2001 From: Do Le Long An <85084360+LaansDole@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:16:04 +0700 Subject: [PATCH 04/42] refactor --- .../add-template-edge-processor/proposal.md | 42 ------- .../specs/edge-processors/spec.md | 110 ------------------ .../add-template-edge-processor/tasks.md | 36 ------ 3 files changed, 188 deletions(-) delete mode 100644 openspec/changes/add-template-edge-processor/proposal.md delete mode 100644 openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md delete mode 100644 openspec/changes/add-template-edge-processor/tasks.md diff --git a/openspec/changes/add-template-edge-processor/proposal.md b/openspec/changes/add-template-edge-processor/proposal.md deleted file mode 100644 index 8f7c0ae29..000000000 --- a/openspec/changes/add-template-edge-processor/proposal.md +++ /dev/null @@ -1,42 +0,0 @@ -# Change: Add Template Edge Processor - -## Why - -The workflow uses `type: template` in edge processors (e.g., `yaml_instance/simulation_hospital.yaml` lines 960, 1006, 1036), but **no template processor implementation exists**. This causes processor resolution failures or silent falls back to regex/function processors. - -Users want to: -1. Use Jinja2 templates for edge payload transformation (data formatting, conditional logic) -2. Separate report templates from LLM prompts (FinalReportAggregator currently has a 50+ line prompt embedding the report structure) -3. Access edge context variables (`input`, `environment.output`, etc.) in templates - -Currently, users must embed all formatting logic in LLM prompts or use regex processors, which is verbose and error-prone. - -## What Changes - -- **ADD** `TemplateEdgeProcessorConfig` dataclass in `entity/configs/edge/edge_processor.py` -- **ADD** `TemplateEdgePayloadProcessor` class in `runtime/edge/processors/template_processor.py` -- **REGISTER** `template` processor type in `runtime/edge/processors/builtin_types.py` -- **ADD** Jinja2 dependency to `pyproject.toml` -- **ADD** unit tests in `tests/test_template_processor.py` - -The processor will: -- Accept `template: str` (Jinja2 template string) -- Provide context variables: `input` (payload), `environment` (global state), `extracted` (from prior processors) -- Support Jinja2 filters: `fromjson`, `tojson`, `default`, standard filters -- Return transformed string payload - -## Impact - -**Affected specs:** -- `edge-processors` (new spec) - -**Affected code:** -- `entity/configs/edge/edge_processor.py` - Add config class -- `runtime/edge/processors/template_processor.py` - New processor implementation -- `runtime/edge/processors/builtin_types.py` - Register processor -- `pyproject.toml` - Add Jinja2 dependency -- `yaml_instance/simulation_hospital.yaml` - Already uses `type: template` (will now work correctly) - -**Breaking changes:** None (adds new functionality) - -**Migration:** Existing workflows using `type: template` will now work correctly instead of failing silently. diff --git a/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md b/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md deleted file mode 100644 index 5b25a607f..000000000 --- a/openspec/changes/add-template-edge-processor/specs/edge-processors/spec.md +++ /dev/null @@ -1,110 +0,0 @@ -## ADDED Requirements - -### Requirement: Template Processor Type Registration - -The system SHALL register a `template` edge processor type that transforms edge payloads using Jinja2 template rendering. - -#### Scenario: Template processor is available -- **WHEN** workflow validation runs or edge processor registry is queried -- **THEN** `template` appears in the list of available processor types -- **AND** processor summary is "Transform payloads using Jinja2 templates" - -### Requirement: Template Configuration - -The template processor SHALL accept a `template` configuration field containing a Jinja2 template string. - -#### Scenario: Valid template configuration -- **WHEN** edge config contains `processor: {type: template, config: {template: "Hello {{ input }}"}}` -- **THEN** configuration parses successfully -- **AND** `TemplateEdgeProcessorConfig` instance is created with template field set - -#### Scenario: Missing template field -- **WHEN** edge config contains `processor: {type: template, config: {}}` -- **THEN** configuration parsing raises `ConfigError` -- **AND** error message indicates `template` field is required - -#### Scenario: Empty template string -- **WHEN** edge config contains `processor: {type: template, config: {template: ""}}` -- **THEN** configuration parsing raises `ConfigError` -- **AND** error message indicates template cannot be empty - -### Requirement: Template Context Variables - -The template processor SHALL provide context variables for use in Jinja2 templates. - -#### Scenario: Input variable access -- **WHEN** template is `"Input: {{ input }}"` -- **AND** edge payload is `"test message"` -- **THEN** processor returns `"Input: test message"` - -#### Scenario: Environment variable access -- **WHEN** template is `"Env: {{ environment.output }}"` -- **AND** environment node output is `"env data"` -- **THEN** processor returns `"Env: env data"` - -#### Scenario: Extracted variable access (from prior processors) -- **WHEN** prior regex processor extracted value `"extracted_value"` -- **AND** template is `"Result: {{ extracted }}"` -- **THEN** processor returns `"Result: extracted_value"` - -### Requirement: Jinja2 Filters - -The template processor SHALL support standard Jinja2 filters plus custom `fromjson` and `tojson` filters. - -#### Scenario: JSON parsing with fromjson filter -- **WHEN** template is `"{% set data = input | fromjson %}Name: {{ data.name }}"` -- **AND** input payload is `'{"name": "Alice"}'` -- **THEN** processor returns `"Name: Alice"` - -#### Scenario: JSON serialization with tojson filter -- **WHEN** template is `"{{ {\"key\": \"value\"} | tojson }}"` -- **THEN** processor returns `"{\"key\": \"value\"}"` - -#### Scenario: Default filter for missing variables -- **WHEN** template is `"Value: {{ missing | default('N/A') }}"` -- **AND** variable `missing` is not in context -- **THEN** processor returns `"Value: N/A"` - -### Requirement: Template Conditional Logic - -The template processor SHALL support Jinja2 control structures (if/for/set). - -#### Scenario: Conditional rendering -- **WHEN** template is `"{% if input == 'yes' %}Confirmed{% else %}Denied{% endif %}"` -- **AND** input is `"yes"` -- **THEN** processor returns `"Confirmed"` - -#### Scenario: Variable assignment -- **WHEN** template is `"{% set x = input | fromjson %}Result: {{ x.field }}"` -- **AND** input is `'{"field": "data"}'` -- **THEN** processor returns `"Result: data"` - -### Requirement: Error Handling - -The template processor SHALL handle template rendering errors gracefully with descriptive messages. - -#### Scenario: Invalid template syntax -- **WHEN** template contains `"{{ unclosed variable"` -- **THEN** processor raises `TemplateRenderError` during initialization -- **AND** error message includes "Invalid template syntax" - -#### Scenario: Undefined variable (strict mode) -- **WHEN** template is `"{{ undefined_var }}"` -- **AND** variable is not in context -- **THEN** processor raises `TemplateRenderError` -- **AND** error message includes variable name and available context keys - -#### Scenario: JSON parsing error -- **WHEN** template uses `"{{ input | fromjson }}"` -- **AND** input is invalid JSON -- **THEN** processor raises `TemplateRenderError` -- **AND** error message includes "JSON decode error" - -### Requirement: Safe Template Execution - -The template processor SHALL use Jinja2 sandboxing to prevent arbitrary code execution. - -#### Scenario: Restricted environment -- **WHEN** template processor initializes Jinja2 environment -- **THEN** environment uses `jinja2.sandbox.SandboxedEnvironment` -- **AND** unsafe operations (file access, imports) are blocked diff --git a/openspec/changes/add-template-edge-processor/tasks.md b/openspec/changes/add-template-edge-processor/tasks.md deleted file mode 100644 index feaaa39c5..000000000 --- a/openspec/changes/add-template-edge-processor/tasks.md +++ /dev/null @@ -1,36 +0,0 @@ -## 1. Implementation - -- [x] 1.1 Add Jinja2 dependency to `pyproject.toml` (`jinja2>=3.1.0`) -- [x] 1.2 Create `TemplateEdgeProcessorConfig` in `entity/configs/edge/edge_processor.py` - - Fields: `template: str` - - Validation: template cannot be empty -- [x] 1.3 Create `TemplateEdgePayloadProcessor` in `runtime/edge/processors/template_processor.py` - - Initialize Jinja2 environment with safe mode - - Register custom filters: `fromjson`, `tojson` - - Implement `process(input, context)` method - - Provide context: `input`, `environment`, `extracted` -- [x] 1.4 Register processor in `runtime/edge/processors/builtin_types.py` - - Type name: `"template"` - - Summary: "Transform payloads using Jinja2 templates" - -## 2. Testing - -- [x] 2.1 Create `tests/test_template_processor.py` - - Test basic variable substitution (`{{ input }}`) - - Test JSON parsing (`{% set data = input | fromjson %}`) - - Test conditional logic (`{% if ... %}`) - - Test environment variable access (`{{ environment.output }}`) - - Test error handling (invalid template, missing variables) -- [x] 2.2 Run validation: `uv run python -m check.check --path yaml_instance/simulation_hospital.yaml` -- [x] 2.3 Run integration test: `uv run pytest tests/test_template_processor.py -v` - -## 3. Documentation - -- [x] 3.1 Add template processor example to `yaml_template/` directory -- [x] 3.2 Update user guide: `docs/user_guide/en/modules/edge_processors.md` (if exists) - N/A: docs don't exist yet - -## 4. Validation - -- [x] 4.1 Verify existing `simulation_hospital.yaml` edges with `type: template` now work -- [x] 4.2 Run full test suite: `uv run pytest` -- [x] 4.3 Validate change: `openspec validate add-template-edge-processor --strict --no-interactive` From 9ae2aec403ed215869944f9cbe6e67cf8a30c812 Mon Sep 17 00:00:00 2001 From: Do Le Long An <85084360+LaansDole@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:20:55 +0700 Subject: [PATCH 05/42] refactor --- yaml_template/template_processor_example.yaml | 86 ------------------- 1 file changed, 86 deletions(-) delete mode 100644 yaml_template/template_processor_example.yaml diff --git a/yaml_template/template_processor_example.yaml b/yaml_template/template_processor_example.yaml deleted file mode 100644 index 212883821..000000000 --- a/yaml_template/template_processor_example.yaml +++ /dev/null @@ -1,86 +0,0 @@ -# Template Edge Processor Example -# -# This workflow demonstrates the template edge processor for transforming -# edge payloads using Jinja2 templates. - -vars: - API_KEY: ${API_KEY} - BASE_URL: ${BASE_URL} - -nodes: - - id: DataSource - type: agent - name: Data Generator - description: Generates sample JSON data - config: - role: | - Generate a JSON object with patient information. - Output format: - { - "name": "John Doe", - "age": 45, - "diagnosis": "Influenza", - "severity": "Medium" - } - provider: openai - name: gpt-4 - api_key: ${API_KEY} - base_url: ${BASE_URL} - - - id: TemplateFormatter - type: agent - name: Report Formatter - description: Formats the report using the template - config: - role: | - You received formatted patient data. Simply output it as-is. - provider: openai - name: gpt-4 - api_key: ${API_KEY} - base_url: ${BASE_URL} - -edges: - # Example 1: Basic variable substitution - - from: DataSource - to: TemplateFormatter - processor: - type: template - config: - template: | - # Patient Report - - **Patient Name:** {{ (input | fromjson).name }} - **Age:** {{ (input | fromjson).age }} - **Diagnosis:** {{ (input | fromjson).diagnosis }} - **Severity:** {{ (input | fromjson).severity }} - - {% set data = input | fromjson %} - {% if data.severity == "High" %} - ⚠️ URGENT: This case requires immediate attention. - {% elif data.severity == "Medium" %} - ℹ️ This case should be reviewed within 24 hours. - {% else %} - ✓ This case is low priority. - {% endif %} - -# Template Features Demonstrated: -# -# 1. JSON Parsing with fromjson filter: -# {% set data = input | fromjson %} -# -# 2. Variable Access: -# {{ data.name }} -# -# 3. Conditional Logic: -# {% if data.severity == "High" %}...{% endif %} -# -# 4. Default Values: -# {{ data.field | default('N/A') }} -# -# 5. For Loops: -# {% for item in data.items %}{{ item }}{% endfor %} -# -# Available Context Variables: -# - input: The edge payload content -# - environment: Global environment state -# - extracted: Value from prior processor (regex, etc.) From 686b059cf82fd298095dea21886fc5fe57fb51d1 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 22:22:46 +0700 Subject: [PATCH 06/42] refactor --- tests/test_template_processor.py | 225 ------------------------------- 1 file changed, 225 deletions(-) delete mode 100644 tests/test_template_processor.py diff --git a/tests/test_template_processor.py b/tests/test_template_processor.py deleted file mode 100644 index 609495cea..000000000 --- a/tests/test_template_processor.py +++ /dev/null @@ -1,225 +0,0 @@ -"""Tests for template edge processor.""" - -import pytest - -from entity.configs.edge.edge_processor import TemplateEdgeProcessorConfig -from entity.configs.base import ConfigError -from entity.messages import Message, MessageRole -from runtime.edge.processors.template_processor import ( - TemplateEdgePayloadProcessor, - TemplateRenderError, -) -from runtime.edge.processors.base import ProcessorFactoryContext -from runtime.node.executor import ExecutionContext -from utils.log_manager import LogManager - - -class MockExecutionContext: - """Mock execution context for testing.""" - - def __init__(self, environment=None): - self.environment = environment or {} - - -def create_processor(template: str) -> TemplateEdgePayloadProcessor: - """Helper to create processor with template.""" - config = TemplateEdgeProcessorConfig(template=template, path="test") - ctx = ProcessorFactoryContext() - return TemplateEdgePayloadProcessor(config, ctx) - - -def transform_text( - processor: TemplateEdgePayloadProcessor, input_text: str, environment=None -) -> str: - """Helper to transform text input.""" - payload = Message(role=MessageRole.USER, content=input_text) - context = MockExecutionContext(environment=environment) - result = processor.transform( - payload, - source_result=payload, - from_node=None, - edge_link=None, - log_manager=LogManager(), - context=context, # type: ignore - ) - return result.text_content() if result else "" - - -class TestTemplateConfiguration: - """Test template configuration validation.""" - - def test_valid_template_configuration(self): - """Valid template configuration should parse successfully.""" - config = TemplateEdgeProcessorConfig(template="Hello {{ input }}", path="test") - assert config.template == "Hello {{ input }}" - - def test_missing_template_field(self): - """Missing template field should raise ConfigError.""" - with pytest.raises(ConfigError, match="expected string"): - TemplateEdgeProcessorConfig.from_dict({}, path="test") - - def test_empty_template_string(self): - """Empty template string should raise ConfigError.""" - with pytest.raises(ConfigError, match="expected non-empty string"): - TemplateEdgeProcessorConfig.from_dict({"template": ""}, path="test") - - -class TestBasicVariableSubstitution: - """Test basic template variable substitution.""" - - def test_input_variable_access(self): - """Template should access input variable.""" - processor = create_processor("Input: {{ input }}") - result = transform_text(processor, "test message") - assert result == "Input: test message" - - def test_environment_variable_access(self): - """Template should access environment variables.""" - processor = create_processor("Env: {{ environment.output }}") - result = transform_text( - processor, "ignored", environment={"output": "env data"} - ) - assert result == "Env: env data" - - def test_extracted_variable_access(self): - """Template should access extracted variable (defaults to input).""" - processor = create_processor("Result: {{ extracted }}") - result = transform_text(processor, "extracted_value") - assert result == "Result: extracted_value" - - -class TestJinja2Filters: - """Test Jinja2 filter support.""" - - def test_fromjson_filter(self): - """Template should parse JSON with fromjson filter.""" - processor = create_processor( - "{% set data = input | fromjson %}Name: {{ data.name }}" - ) - result = transform_text(processor, '{"name": "Alice"}') - assert result == "Name: Alice" - - def test_tojson_filter(self): - """Template should serialize to JSON with tojson filter.""" - processor = create_processor('{{ {"key": "value"} | tojson }}') - result = transform_text(processor, "ignored") - assert result == '{"key": "value"}' - - def test_default_filter_for_missing_variables(self): - """Template should use default value for missing variables.""" - processor = create_processor("Value: {{ missing | default('N/A') }}") - result = transform_text(processor, "ignored") - assert result == "Value: N/A" - - def test_fromjson_invalid_json(self): - """fromjson filter should raise error on invalid JSON.""" - processor = create_processor("{{ input | fromjson }}") - with pytest.raises(TemplateRenderError, match="JSON decode error"): - transform_text(processor, "not valid json") - - -class TestConditionalLogic: - """Test Jinja2 control structures.""" - - def test_conditional_rendering(self): - """Template should support if/else conditionals.""" - processor = create_processor( - "{% if input == 'yes' %}Confirmed{% else %}Denied{% endif %}" - ) - assert transform_text(processor, "yes") == "Confirmed" - assert transform_text(processor, "no") == "Denied" - - def test_variable_assignment(self): - """Template should support variable assignment with set.""" - processor = create_processor( - "{% set x = input | fromjson %}Result: {{ x.field }}" - ) - result = transform_text(processor, '{"field": "data"}') - assert result == "Result: data" - - def test_for_loop(self): - """Template should support for loops.""" - processor = create_processor( - "{% set items = input | fromjson %}{% for item in items %}{{ item }}{% if not loop.last %},{% endif %}{% endfor %}" - ) - result = transform_text(processor, '["a", "b", "c"]') - assert result == "a,b,c" - - -class TestErrorHandling: - """Test template error handling.""" - - def test_invalid_template_syntax(self): - """Invalid template syntax should raise error during initialization.""" - config = TemplateEdgeProcessorConfig( - template="{{ unclosed variable", path="test" - ) - ctx = ProcessorFactoryContext() - with pytest.raises(TemplateRenderError, match="Invalid template syntax"): - TemplateEdgePayloadProcessor(config, ctx) - - def test_undefined_variable_strict_mode(self): - """Undefined variable should raise error with available context keys.""" - processor = create_processor("{{ undefined_var }}") - with pytest.raises( - TemplateRenderError, match="Undefined variable.*Available context keys" - ): - transform_text(processor, "test") - - -class TestComplexScenarios: - """Test complex real-world template scenarios.""" - - def test_medical_report_template(self): - """Template should handle complex medical report formatting.""" - template = """{% set env = input | fromjson %} -# Medical Report: {{ env.outbreak }} - -Total Patients: {{ env.total_patients }} - -{% for patient in env.patients %} -## Patient {{ loop.index }}: {{ patient.name }} -- Diagnosis: {{ patient.diagnosis | default('Pending') }} -{% endfor %}""" - - processor = create_processor(template) - input_data = { - "outbreak": "COVID-19", - "total_patients": 2, - "patients": [{"name": "Alice", "diagnosis": "Influenza"}, {"name": "Bob"}], - } - - import json - - result = transform_text(processor, json.dumps(input_data)) - - assert "# Medical Report: COVID-19" in result - assert "Total Patients: 2" in result - assert "## Patient 1: Alice" in result - assert "- Diagnosis: Influenza" in result - assert "## Patient 2: Bob" in result - assert "- Diagnosis: Pending" in result - - def test_environment_context_formatting(self): - """Template should format data from environment context.""" - template = """{% set env = environment.output | fromjson %} -OUTBREAK: {{ env.outbreak }} -URGENCY: {{ env.urgency_level }} -CONDITIONS: {{ env.atmospheric_description }}""" - - processor = create_processor(template) - env_data = { - "outbreak": "COVID-19 pandemic", - "urgency_level": "High", - "atmospheric_description": "Hospital overflowing", - } - - import json - - result = transform_text( - processor, "ignored", environment={"output": json.dumps(env_data)} - ) - - assert "OUTBREAK: COVID-19 pandemic" in result - assert "URGENCY: High" in result - assert "CONDITIONS: Hospital overflowing" in result From cbc9ab31392cbad84b38f110125cc863b6825492 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 22:32:05 +0700 Subject: [PATCH 07/42] refactor --- .github/workflows/validate-yamls.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/validate-yamls.yml b/.github/workflows/validate-yamls.yml index 7967c11e8..50630cebe 100644 --- a/.github/workflows/validate-yamls.yml +++ b/.github/workflows/validate-yamls.yml @@ -43,16 +43,6 @@ jobs: sudo apt-get update sudo apt-get install -y libcairo2-dev pkg-config - - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Install system dependencies for pycairo - run: | - sudo apt-get update - sudo apt-get install -y libcairo2-dev pkg-config - - name: Cache uv dependencies uses: actions/cache@v4 with: @@ -77,4 +67,4 @@ jobs: else echo "YAML validation failed - check the logs above for details" exit 1 - fi + fi \ No newline at end of file From 642e625d9969a41027ac7663719495ef3b40f773 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 15:30:15 +0700 Subject: [PATCH 08/42] chores: uv lock --- uv.lock | 527 +++++++++++++++++++++++++------------------------------- 1 file changed, 239 insertions(+), 288 deletions(-) diff --git a/uv.lock b/uv.lock index b09d58d69..e9f501c5b 100755 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = "==3.12.*" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "annotated-doc" @@ -22,15 +27,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] @@ -62,14 +67,14 @@ wheels = [ [[package]] name = "authlib" -version = "1.6.6" +version = "1.6.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/dc/ed1681bf1339dd6ea1ce56136bad4baabc6f7ad466e375810702b0237047/authlib-1.6.7.tar.gz", hash = "sha256:dbf10100011d1e1b34048c9d120e83f13b35d69a826ae762b93d2fb5aafc337b", size = 164950, upload-time = "2026-02-06T14:04:14.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, + { url = "https://files.pythonhosted.org/packages/f8/00/3ed12264094ec91f534fae429945efbaa9f8c666f3aa7061cc3b2a26a0cd/authlib-1.6.7-py2.py3-none-any.whl", hash = "sha256:c637340d9a02789d2efa1d003a7437d10d3e565237bcb5fcbc6c134c7b95bab0", size = 244115, upload-time = "2026-02-06T14:04:12.141Z" }, ] [[package]] @@ -130,11 +135,11 @@ wheels = [ [[package]] name = "cachetools" -version = "6.2.4" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, + { url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" }, ] [[package]] @@ -159,11 +164,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -275,45 +280,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, ] +[[package]] +name = "croniter" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/2f/44d1ae153a0e27be56be43465e5cb39b9650c781e001e7864389deb25090/croniter-6.0.0.tar.gz", hash = "sha256:37c504b313956114a983ece2c2b07790b1f1094fe9d81cc94739214748255577", size = 64481, upload-time = "2024-12-17T17:17:47.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/4b/290b4c3efd6417a8b0c284896de19b1d5855e6dbdb97d2a35e68fa42de85/croniter-6.0.0-py2.py3-none-any.whl", hash = "sha256:2f878c3856f17896979b2a4379ba1f09c83e374931ea15cc835c5dd2eee9b368", size = 25468, upload-time = "2024-12-17T17:17:45.359Z" }, +] + [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, ] [[package]] @@ -340,7 +356,7 @@ wheels = [ [[package]] name = "cyclopts" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -348,9 +364,9 @@ dependencies = [ { name = "rich" }, { name = "rich-rst" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/3a/fd746469c7000ccaa75787e8ebd60dc77e4541576ca4ed241cd8b9e7e9ad/cyclopts-4.4.0.tar.gz", hash = "sha256:16764f5a807696b61da7d19626f34d261cdffe33345e87a194cf3286db2bd9cc", size = 158378, upload-time = "2025-12-16T14:03:09.799Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/18/5ca04dfda3e53b5d07b072033cc9f7bf10f93f78019366bff411433690d1/cyclopts-4.4.0-py3-none-any.whl", hash = "sha256:78ff95a5e52e738a1d0f01e5a3af48049c47748fa2c255f2629a4cef54dcf2b3", size = 195801, upload-time = "2025-12-16T14:03:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" }, ] [[package]] @@ -521,21 +537,21 @@ wheels = [ [[package]] name = "faiss-cpu" -version = "1.13.1" +version = "1.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "packaging" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/66/92/c4f30580aee11fda3f424f8509d9b5ad96b9f44409f52a7ceb6b42880e50/faiss_cpu-1.13.1-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:2967def7aa2da8efbf6a5da81138780aa17a9970ca666417cb632a00a593423d", size = 3418004, upload-time = "2025-12-05T01:01:51.955Z" }, - { url = "https://files.pythonhosted.org/packages/04/1f/30803e63affa8bbdfd549f83ed5d39ccf900c030b6da8010d0b95f7ae1d1/faiss_cpu-1.13.1-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:30c179891656a988f5223e586c696432aacc5f4e763d85e165be30ef57ac2bbf", size = 7806468, upload-time = "2025-12-05T01:01:54.096Z" }, - { url = "https://files.pythonhosted.org/packages/17/ae/40f66b640664af319ff8be87a9b0cc2c9ec025a2cf82b27cc27964fcf3c0/faiss_cpu-1.13.1-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff5bdbf392081659e6b0f98f03b602bf08d1b5a790e28aa1185ae925decff6b2", size = 11410471, upload-time = "2025-12-05T01:01:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/b8f0862ec6af8a71c6410a61baa35571161f7dba616aed696e91cb464630/faiss_cpu-1.13.1-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3de25edb0e69c1b95eeda923b2e23da01f472b2cc3f4817e63b25a56847d6ea7", size = 23719213, upload-time = "2025-12-05T01:01:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ee/01e07e4e780b0b739a3299ca8e5b4751970629b0f2c51f5ec464718e9f9e/faiss_cpu-1.13.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0b2f0e6cd30511b9fe320a2309389269269d3e363cc88c3a0380095a8c08ae27", size = 13400767, upload-time = "2025-12-05T01:02:00.742Z" }, - { url = "https://files.pythonhosted.org/packages/da/27/0c4e249fe50f87f1f038c80deebcdd28b23617bb42e3e5708b34c86fdae7/faiss_cpu-1.13.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ad542573ad05af6c508f4cf5268ba2aad06f0c8d4e780a0eeba7fe6fd274922", size = 24960102, upload-time = "2025-12-05T01:02:04.56Z" }, - { url = "https://files.pythonhosted.org/packages/09/bc/ce942b00958ef52caca71666c06fa801fcd99dc61a9873ab067932dd3d5e/faiss_cpu-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:0fece5b63e8d014f8db4abfe0b4c9a82e6508e64f450fce700e5cb4b47041f1a", size = 18812863, upload-time = "2025-12-05T01:02:14.982Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7b91c9cb328d960466e23cd9ca02f44d554ac5761d41262b74daa1715da1/faiss_cpu-1.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:168986e3f152a7568257c5ac50f3cf1a1aaa34fb41e1ba7259799bcb8ffe687f", size = 8507940, upload-time = "2025-12-05T01:02:18.078Z" }, + { url = "https://files.pythonhosted.org/packages/07/c9/671f66f6b31ec48e5825d36435f0cb91189fa8bb6b50724029dbff4ca83c/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1", size = 3452776, upload-time = "2025-12-24T10:27:01.419Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4a/97150aa1582fb9c2bca95bd8fc37f27d3b470acec6f0a6833844b21e4b40/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317", size = 7896434, upload-time = "2025-12-24T10:27:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d0/0940575f059591ca31b63a881058adb16a387020af1709dcb7669460115c/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1", size = 11485825, upload-time = "2025-12-24T10:27:05.681Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e1/a5acac02aa593809f0123539afe7b4aff61d1db149e7093239888c9053e1/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7", size = 23845772, upload-time = "2025-12-24T10:27:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7b/49dcaf354834ec457e85ca769d50bc9b5f3003fab7c94a9dcf08cf742793/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f", size = 13477567, upload-time = "2025-12-24T10:27:10.822Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6b/12bb4037921c38bb2c0b4cfc213ca7e04bbbebbfea89b0b5746248ce446e/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f", size = 25102239, upload-time = "2025-12-24T10:27:13.476Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/35ed875423200c17bdd594ce921abfc1812ddd21e09355290b9a94e170ab/faiss_cpu-1.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:b82c01d30430dd7b1fa442001b9099735d1a82f6bb72033acdc9206d5ac66a64", size = 18890300, upload-time = "2025-12-24T10:27:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/bbdf5deaf6feb34b46b469c0a0acd40216c3d3c6ecf5aeb71d56b8a650e3/faiss_cpu-1.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2c4f696ae76e7c97cbc12311db83aaf1e7f4f7be06a3ffea7e5b0e8ec1fd805b", size = 8553157, upload-time = "2025-12-24T10:27:26.38Z" }, ] [[package]] @@ -582,16 +598,18 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.14.1" +version = "2.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, { name = "openapi-pydantic" }, + { name = "packaging" }, { name = "platformdirs" }, { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, @@ -602,18 +620,18 @@ dependencies = [ { name = "uvicorn" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/50/d38e4371bdc34e709f4731b1e882cb7bc50e51c1a224859d4cd381b3a79b/fastmcp-2.14.1.tar.gz", hash = "sha256:132725cbf77b68fa3c3d165eff0cfa47e40c1479457419e6a2cfda65bd84c8d6", size = 8263331, upload-time = "2025-12-15T02:26:27.102Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/32/982678d44f13849530a74ab101ed80e060c2ee6cf87471f062dcf61705fd/fastmcp-2.14.5.tar.gz", hash = "sha256:38944dc582c541d55357082bda2241cedb42cd3a78faea8a9d6a2662c62a42d7", size = 8296329, upload-time = "2026-02-03T15:35:21.005Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/82/72401d09dc27c27fdf72ad6c2fe331e553e3c3646e01b5ff16473191033d/fastmcp-2.14.1-py3-none-any.whl", hash = "sha256:fb3e365cc1d52573ab89caeba9944dd4b056149097be169bce428e011f0a57e5", size = 412176, upload-time = "2025-12-15T02:26:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c1/1a35ec68ff76ea8443aa115b18bcdee748a4ada2124537ee90522899ff9f/fastmcp-2.14.5-py3-none-any.whl", hash = "sha256:d81e8ec813f5089d3624bec93944beaefa86c0c3a4ef1111cbef676a761ebccf", size = 417784, upload-time = "2026-02-03T15:35:18.489Z" }, ] [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -649,16 +667,16 @@ wheels = [ [[package]] name = "google-auth" -version = "2.45.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, + { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708, upload-time = "2025-12-15T22:58:42.889Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312, upload-time = "2025-12-15T22:58:40.777Z" }, + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, ] [package.optional-dependencies] @@ -668,7 +686,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.56.0" +version = "1.62.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -682,9 +700,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/ad/d3ac5a102135bd3f1e4b1475ca65d2bd4bcc22eb2e9348ac40fe3fadb1d6/google_genai-1.56.0.tar.gz", hash = "sha256:0491af33c375f099777ae207d9621f044e27091fafad4c50e617eba32165e82f", size = 340451, upload-time = "2025-12-17T12:35:05.412Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/4c/71b32b5c8db420cf2fd0d5ef8a672adbde97d85e5d44a0b4fca712264ef1/google_genai-1.62.0.tar.gz", hash = "sha256:709468a14c739a080bc240a4f3191df597bf64485b1ca3728e0fb67517774c18", size = 490888, upload-time = "2026-02-04T22:48:41.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/93/94bc7a89ef4e7ed3666add55cd859d1483a22737251df659bf1aa46e9405/google_genai-1.56.0-py3-none-any.whl", hash = "sha256:9e6b11e0c105ead229368cb5849a480e4d0185519f8d9f538d61ecfcf193b052", size = 426563, upload-time = "2025-12-17T12:35:03.717Z" }, + { url = "https://files.pythonhosted.org/packages/09/5f/4645d8a28c6e431d0dd6011003a852563f3da7037d36af53154925b099fd/google_genai-1.62.0-py3-none-any.whl", hash = "sha256:4c3daeff3d05fafee4b9a1a31f9c07f01bc22051081aa58b4d61f58d16d1bcc0", size = 724166, upload-time = "2026-02-04T22:48:39.956Z" }, ] [[package]] @@ -800,14 +818,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -833,23 +851,23 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" }, ] [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] @@ -875,32 +893,41 @@ wheels = [ [[package]] name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[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 = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -908,9 +935,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -1025,11 +1052,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" }, ] [[package]] @@ -1091,7 +1118,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.25.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1109,9 +1136,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -1143,26 +1170,26 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.5" +version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, ] [[package]] name = "openai" -version = "2.14.0" +version = "2.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1174,9 +1201,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/b1/12fe1c196bea326261718eb037307c1c1fe1dedc2d2d4de777df822e6238/openai-2.14.0.tar.gz", hash = "sha256:419357bedde9402d23bf8f2ee372fca1985a73348debba94bddff06f19459952", size = 626938, upload-time = "2025-12-19T03:28:45.742Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/7c1a00c2c3fbd004253937f7520f692a9650767aa73894d7a34f0d65d3f4/openai-2.14.0-py3-none-any.whl", hash = "sha256:7ea40aca4ffc4c4a776e77679021b47eec1160e341f42ae086ba949c9dcc9183", size = 1067558, upload-time = "2025-12-19T03:28:43.727Z" }, + { url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" }, ] [[package]] @@ -1216,62 +1243,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] -[[package]] -name = "opentelemetry-exporter-prometheus" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "prometheus-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, -] - -[[package]] -name = "opentelemetry-instrumentation" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, -] - [[package]] name = "oscrypto" version = "1.3.0" @@ -1286,32 +1257,32 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, ] [[package]] @@ -1334,21 +1305,21 @@ wheels = [ [[package]] name = "pillow" -version = "12.0.0" +version = "12.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, - { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, - { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, - { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, - { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, - { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/dc53fe21a2f2996e1b7d92bf671cdb157079385183ef7c1ae08b485db510/pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b", size = 5262642, upload-time = "2026-01-02T09:11:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c1/10e45ac9cc79419cedf5121b42dcca5a50ad2b601fa080f58c22fb27626e/pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551", size = 4657464, upload-time = "2026-01-02T09:11:12.319Z" }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b82c0ab7ef40ebede7a97c72d473bda5950f609f8e0c77b04af574a0ddb/pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208", size = 6234878, upload-time = "2026-01-02T09:11:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/27abc9792615b5e886ca9411ba6637b675f1b77af3104710ac7353fe5605/pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5", size = 8044868, upload-time = "2026-01-02T09:11:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ea/f200a4c36d836100e7bc738fc48cd963d3ba6372ebc8298a889e0cfc3359/pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661", size = 6349468, upload-time = "2026-01-02T09:11:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/48d0b77ab2200374c66d344459b8958c86693be99526450e7aee714e03e4/pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17", size = 7041518, upload-time = "2026-01-02T09:11:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/1d/23/c281182eb986b5d31f0a76d2a2c8cd41722d6fb8ed07521e802f9bba52de/pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670", size = 6462829, upload-time = "2026-01-02T09:11:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ef/7018273e0faac099d7b00982abdcc39142ae6f3bd9ceb06de09779c4a9d6/pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616", size = 7166756, upload-time = "2026-01-02T09:11:23.559Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c8/993d4b7ab2e341fe02ceef9576afcf5830cdec640be2ac5bee1820d693d4/pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7", size = 6328770, upload-time = "2026-01-02T09:11:25.661Z" }, + { url = "https://files.pythonhosted.org/packages/a7/87/90b358775a3f02765d87655237229ba64a997b87efa8ccaca7dd3e36e7a7/pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d", size = 7033406, upload-time = "2026-01-02T09:11:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" }, ] [[package]] @@ -1387,11 +1358,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -1437,11 +1408,11 @@ wheels = [ [[package]] name = "pyasn1" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] @@ -1469,11 +1440,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -1541,14 +1512,13 @@ wheels = [ [[package]] name = "pydocket" -version = "0.16.1" +version = "0.17.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, + { name = "croniter" }, { name = "fakeredis", extra = ["lua"] }, { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-prometheus" }, - { name = "opentelemetry-instrumentation" }, { name = "prometheus-client" }, { name = "py-key-value-aio", extra = ["memory", "redis"] }, { name = "python-json-logger" }, @@ -1557,9 +1527,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/ff/87e931e4abc7efb1e8c16adaa55327452931e8c5f460f2ba089447673226/pydocket-0.16.1.tar.gz", hash = "sha256:8663cb6dc801d8b8d703541fb665f4099c84f4d10d8f3fd441e208b080aa4826", size = 289028, upload-time = "2025-12-19T19:43:48.773Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/26/ac23ead3725475468b50b486939bf5feda27180050a614a7407344a0af0e/pydocket-0.17.5.tar.gz", hash = "sha256:19a6976d8fd11c1acf62feb0291a339e06beaefa100f73dd38c6499760ad3e62", size = 334829, upload-time = "2026-01-30T18:44:39.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/ab/0da7d0397112546309709f464bdf65de4e1697e3caba07556751fc4d8bcd/pydocket-0.16.1-py3-none-any.whl", hash = "sha256:bc6ccf7e91164761def854b4014101abf23c3cc2fb7d0fa2c4e07ea3bf6a1826", size = 63208, upload-time = "2025-12-19T19:43:47.309Z" }, + { url = "https://files.pythonhosted.org/packages/14/98/73427d065c067a99de6afbe24df3d90cf20d63152ceb42edff2b6e829d4c/pydocket-0.17.5-py3-none-any.whl", hash = "sha256:544d7c2625a33e52528ac24db25794841427dfc2cf30b9c558ac387c77746241", size = 93355, upload-time = "2026-01-30T18:44:37.972Z" }, ] [[package]] @@ -1588,7 +1558,7 @@ wheels = [ [[package]] name = "pyhanko" -version = "0.32.0" +version = "0.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asn1crypto" }, @@ -1599,14 +1569,14 @@ dependencies = [ { name = "requests" }, { name = "tzlocal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d5/9be7c6a62d9fe9ad824f4a3144e7f55bff11198446fe4ec7f96cd3f9673c/pyhanko-0.32.0.tar.gz", hash = "sha256:47df283f14289b9df72071c3e5c52c426998f1850e21bff9d6451e4369595820", size = 411900, upload-time = "2025-11-22T16:22:07.543Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/9f/c8baf04b8aaadf099d8f12b26fe57d7b0b6842179160e5e3099c56d06bac/pyhanko-0.33.0.tar.gz", hash = "sha256:68ea123efd6612420fd2f1856c0b7a4bfa70f4af0abc0ddb329416844f5befb6", size = 412604, upload-time = "2026-02-08T07:21:55.209Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/32/ca5eeaf212f90be6fb89bfa443879d8a24fd952f9763eeaa976e60593dd9/pyhanko-0.32.0-py3-none-any.whl", hash = "sha256:04bb04e7791aeed626e32264b2eb273ed73104f7ad3a47e27a69b7faedfd3451", size = 470706, upload-time = "2025-11-22T16:22:05.477Z" }, + { url = "https://files.pythonhosted.org/packages/d1/47/c25a4849951255b50ef0662dc04fa7227120ed31fb6aed398946ae164f46/pyhanko-0.33.0-py3-none-any.whl", hash = "sha256:3461a6c803a20b5b9847523ef991fd3f09bb612f41ec71dbb70128ce7651dbab", size = 471693, upload-time = "2026-02-08T07:21:53.367Z" }, ] [[package]] name = "pyhanko-certvalidator" -version = "0.29.0" +version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asn1crypto" }, @@ -1615,18 +1585,18 @@ dependencies = [ { name = "requests" }, { name = "uritools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/3d/3e41e3b492b84e327c2340c380439c6805f9d2a3b574f662041958f19373/pyhanko_certvalidator-0.29.0.tar.gz", hash = "sha256:8ebb98e742e4a2e2347374535c1329abf77c398addb31e623f33645ace02efa4", size = 93212, upload-time = "2025-09-12T22:05:37.207Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/f6/5964e64ccf72f305e56d49014f8fae068c75cbf26e771a39ada64850054e/pyhanko_certvalidator-0.29.1.tar.gz", hash = "sha256:e8a8ad40eb73f4a32a4c4ce4474af6514bf86bc72e0fbb339d2b473f8d57e2a2", size = 93217, upload-time = "2026-02-08T07:08:20.475Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ba/a4e8fb3c43fd238f329f92996c7a1940ec1c0da005095c19dc91ea94bbe3/pyhanko_certvalidator-0.29.0-py3-none-any.whl", hash = "sha256:567c609900149d133aa30fcf0efb7128ca5915b58bf899705441726a0618925f", size = 111754, upload-time = "2025-09-12T22:05:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ef/88954393b07d6b814d4963722c39460e256df04b4e7fa434e8390ebcc364/pyhanko_certvalidator-0.29.1-py3-none-any.whl", hash = "sha256:c85747bb09bfb23777f947901212a6640f5d1ee3bd95479b063b27c2f4213250", size = 111767, upload-time = "2026-02-08T07:08:19.243Z" }, ] [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.11.0" 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" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] @@ -1636,20 +1606,20 @@ crypto = [ [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pypdf" -version = "6.5.0" +version = "6.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/9b/db1056a54eda8cd44f9e5128e87e1142cb328295dad92bbec0d39f251641/pypdf-6.5.0.tar.gz", hash = "sha256:9e78950906380ae4f2ce1d9039e9008098ba6366a4d9c7423c4bdbd6e6683404", size = 5277655, upload-time = "2025-12-21T11:07:19.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/45/8340de1c752bfda2da912ea0fa8c9a432f7de3f6315e82f1c0847811dff6/pypdf-6.7.0.tar.gz", hash = "sha256:eb95e244d9f434e6cfd157272283339ef586e593be64ee699c620f756d5c3f7e", size = 5299947, upload-time = "2026-02-08T14:47:11.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/db/f2e7703791a1f32532618b82789ddddb7173b9e22d97e34cc11950d8e330/pypdf-6.5.0-py3-none-any.whl", hash = "sha256:9cef8002aaedeecf648dfd9ff1ce38f20ae8d88e2534fced6630038906440b25", size = 329560, upload-time = "2025-12-21T11:07:18.173Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f1/c92e75a0eb18bb10845e792054ded113010de958b6d4998e201c029417bb/pypdf-6.7.0-py3-none-any.whl", hash = "sha256:62e85036d50839cbdf45b8067c2c1a1b925517514d7cba4cbe8755a6c2829bc9", size = 330557, upload-time = "2026-02-08T14:47:10.111Z" }, ] [[package]] @@ -1760,11 +1730,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -1838,15 +1808,15 @@ wheels = [ [[package]] name = "reportlab" -version = "4.4.7" +version = "4.4.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/a7/4600cb1cfc975a06552e8927844ddcb8fd90217e9a6068f5c7aa76c3f221/reportlab-4.4.7.tar.gz", hash = "sha256:41e8287af965e5996764933f3e75e7f363c3b6f252ba172f9429e81658d7b170", size = 3714000, upload-time = "2025-12-21T11:50:11.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/39/42cf24aee570a80e1903221ae3a92a2e34c324794a392eb036cbb6dc3839/reportlab-4.4.9.tar.gz", hash = "sha256:7cf487764294ee791a4781f5a157bebce262a666ae4bbb87786760a9676c9378", size = 3911246, upload-time = "2026-01-15T10:07:56.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/bf/a29507386366ab17306b187ad247dd78e4599be9032cb5f44c940f547fc0/reportlab-4.4.7-py3-none-any.whl", hash = "sha256:8fa05cbf468e0e76745caf2029a4770276edb3c8e86a0b71e0398926baf50673", size = 1954263, upload-time = "2025-12-21T11:50:08.93Z" }, + { url = "https://files.pythonhosted.org/packages/17/77/546e50edfaba6a0e58e8ec5fdc4446510227cec9e8f40172b60941d5a633/reportlab-4.4.9-py3-none-any.whl", hash = "sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd", size = 1954401, upload-time = "2026-01-15T09:27:59.133Z" }, ] [[package]] @@ -1866,15 +1836,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] @@ -1957,8 +1927,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "jeepney", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -2031,24 +2001,24 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sse-starlette" -version = "3.0.4" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/8b/54651ad49bce99a50fd61a7f19c2b6a79fbb072e693101fbb1194c362054/sse_starlette-3.0.4.tar.gz", hash = "sha256:5e34286862e96ead0eb70f5ddd0bd21ab1f6473a8f44419dd267f431611383dd", size = 22576, upload-time = "2025-12-14T16:22:52.493Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/22/8ab1066358601163e1ac732837adba3672f703818f693e179b24e0d3b65c/sse_starlette-3.0.4-py3-none-any.whl", hash = "sha256:32c80ef0d04506ced4b0b6ab8fe300925edc37d26f666afb1874c754895f5dc3", size = 11764, upload-time = "2025-12-14T16:22:51.453Z" }, + { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, ] [[package]] @@ -2082,11 +2052,11 @@ wheels = [ [[package]] name = "tenacity" -version = "9.1.2" +version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] @@ -2103,19 +2073,19 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "typer" -version = "0.20.1" +version = "0.21.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2123,9 +2093,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c1/933d30fd7a123ed981e2a1eedafceab63cb379db0402e438a13bc51bbb15/typer-0.20.1.tar.gz", hash = "sha256:68585eb1b01203689c4199bc440d6be616f0851e9f0eb41e4a778845c5a0fd5b", size = 105968, upload-time = "2025-12-19T16:48:56.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/52/1f2df7e7d1be3d65ddc2936d820d4a3d9777a54f4204f5ca46b8513eff77/typer-0.20.1-py3-none-any.whl", hash = "sha256:4b3bde918a67c8e03d861aa02deca90a95bbac572e71b1b9be56ff49affdb5a8", size = 47381, upload-time = "2025-12-19T16:48:53.679Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, ] [[package]] @@ -2181,24 +2151,24 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [[package]] @@ -2230,25 +2200,6 @@ wheels = [ { 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.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - [[package]] name = "wsproto" version = "1.3.2" From 3b60179c685adfb985ff35f1d1adb83c69449403 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 20:55:00 +0700 Subject: [PATCH 09/42] feat: add template node for Jinja2-based formatting Add template node type that formats input messages using Jinja2 templates and emits rendered output directly to logs (without LLM interaction). Implementation: - entity/configs/node/template.py: Configuration schema with template field - runtime/node/executor/template_executor.py: Executor with sandboxed Jinja2 - runtime/node/builtin_nodes.py: Register template node type - yaml_instance/template_demo.yaml: Enhanced demo showing both edge processors and template nodes Features: - Sandboxed Jinja2 environment with StrictUndefined mode - Custom filters: fromjson, tojson (plus all standard Jinja2 filters) - Template context: input (latest message), environment (execution vars) - Error handling with detailed logging - Supports conditionals, loops, string operations Use case: - Format structured data (JSON) into human-readable reports - Create discharge summaries, dashboards, formatted logs - Deterministic formatting without AI reasoning --- entity/configs/node/template.py | 42 +++++ runtime/node/builtin_nodes.py | 21 ++- runtime/node/executor/template_executor.py | 124 +++++++++++++++ yaml_instance/template_demo.yaml | 174 ++++++++++++++++++++- 4 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 entity/configs/node/template.py create mode 100644 runtime/node/executor/template_executor.py diff --git a/entity/configs/node/template.py b/entity/configs/node/template.py new file mode 100644 index 000000000..e3a7ee889 --- /dev/null +++ b/entity/configs/node/template.py @@ -0,0 +1,42 @@ +"""Configuration for template nodes.""" + +from dataclasses import dataclass +from typing import Mapping, Any + +from entity.configs.base import ( + BaseConfig, + ConfigError, + ConfigFieldSpec, + require_mapping, + require_str, +) + + +@dataclass +class TemplateNodeConfig(BaseConfig): + """Config describing the Jinja2 template used to format output.""" + + template: str = "" + + @classmethod + def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "TemplateNodeConfig": + mapping = require_mapping(data, path) + template = require_str(mapping, "template", path) + if not template: + raise ConfigError("template cannot be empty", f"{path}.template") + + return cls(template=template, path=path) + + def validate(self) -> None: + if not self.template: + raise ConfigError("template cannot be empty", f"{self.path}.template") + + FIELD_SPECS = { + "template": ConfigFieldSpec( + name="template", + display_name="Jinja2 Template", + type_hint="text", + required=True, + description="Jinja2 template string for formatting output. Available context: {{ input }} (latest message content), {{ environment }} (execution environment variables).", + ), + } diff --git a/runtime/node/builtin_nodes.py b/runtime/node/builtin_nodes.py index 2a2311156..a3ef6ac01 100755 --- a/runtime/node/builtin_nodes.py +++ b/runtime/node/builtin_nodes.py @@ -12,6 +12,7 @@ from entity.configs.node.literal import LiteralNodeConfig from entity.configs.node.python_runner import PythonRunnerConfig from entity.configs.node.loop_counter import LoopCounterConfig +from entity.configs.node.template import TemplateNodeConfig from runtime.node.executor.agent_executor import AgentNodeExecutor from runtime.node.executor.human_executor import HumanNodeExecutor from runtime.node.executor.passthrough_executor import PassthroughNodeExecutor @@ -19,6 +20,7 @@ from runtime.node.executor.python_executor import PythonNodeExecutor from runtime.node.executor.subgraph_executor import SubgraphNodeExecutor from runtime.node.executor.loop_counter_executor import LoopCounterNodeExecutor +from runtime.node.executor.template_executor import TemplateNodeExecutor from runtime.node.registry import NodeCapabilities, register_node_type @@ -48,9 +50,10 @@ "subgraph", config_cls=SubgraphConfig, executor_cls=SubgraphNodeExecutor, - capabilities=NodeCapabilities( + capabilities=NodeCapabilities(), + executor_factory=lambda context, subgraphs=None: SubgraphNodeExecutor( + context, subgraphs or {} ), - executor_factory=lambda context, subgraphs=None: SubgraphNodeExecutor(context, subgraphs or {}), summary="Embeds (through file path or inline config) and runs another named subgraph within the current workflow", ) @@ -69,8 +72,7 @@ "passthrough", config_cls=PassthroughConfig, executor_cls=PassthroughNodeExecutor, - capabilities=NodeCapabilities( - ), + capabilities=NodeCapabilities(), summary="Forwards prior node output downstream without modification", ) @@ -78,8 +80,7 @@ "literal", config_cls=LiteralNodeConfig, executor_cls=LiteralNodeExecutor, - capabilities=NodeCapabilities( - ), + capabilities=NodeCapabilities(), summary="Emits the configured text message every time it is triggered", ) @@ -91,6 +92,14 @@ summary="Blocks downstream edges until the configured iteration limit is reached, then emits a message to release the loop.", ) +register_node_type( + "template", + config_cls=TemplateNodeConfig, + executor_cls=TemplateNodeExecutor, + capabilities=NodeCapabilities(), + summary="Formats input messages using Jinja2 templates and emits the rendered output", +) + # Register subgraph source types (file-based and inline config) register_subgraph_source( "config", diff --git a/runtime/node/executor/template_executor.py b/runtime/node/executor/template_executor.py new file mode 100644 index 000000000..a1e9391a6 --- /dev/null +++ b/runtime/node/executor/template_executor.py @@ -0,0 +1,124 @@ +"""Template node executor.""" + +import json +from typing import List, Any + +from jinja2 import TemplateSyntaxError, UndefinedError, StrictUndefined +from jinja2.sandbox import SandboxedEnvironment + +from entity.configs import Node +from entity.configs.node.template import TemplateNodeConfig +from entity.messages import Message, MessageRole +from runtime.node.executor.base import NodeExecutor + + +class TemplateRenderError(Exception): + """Raised when template rendering fails.""" + + pass + + +def _fromjson_filter(value: str) -> Any: + """Parse JSON string into Python object.""" + try: + return json.loads(value) + except json.JSONDecodeError as exc: + raise TemplateRenderError(f"JSON decode error: {exc}") from exc + + +def _tojson_filter(value: Any) -> str: + """Serialize Python object to JSON string.""" + try: + return json.dumps(value, ensure_ascii=False) + except (TypeError, ValueError) as exc: + raise TemplateRenderError(f"JSON encode error: {exc}") from exc + + +class TemplateNodeExecutor(NodeExecutor): + """Format input messages using Jinja2 templates and emit the result.""" + + def execute(self, node: Node, inputs: List[Message]) -> List[Message]: + if node.node_type != "template": + raise ValueError(f"Node {node.id} is not a template node") + + config = node.as_config(TemplateNodeConfig) + if config is None: + raise ValueError(f"Node {node.id} missing template configuration") + + self._ensure_not_cancelled() + + # Handle empty inputs - return empty message + if not inputs: + warning_msg = f"Template node '{node.id}' triggered without inputs" + self.log_manager.warning( + warning_msg, node_id=node.id, details={"input_count": 0} + ) + return [Message(content="", role=MessageRole.USER)] + + # Get latest input message (consistent with passthrough node behavior) + latest_input = inputs[-1] + input_text = latest_input.text_content + + if len(inputs) > 1: + self.log_manager.debug( + f"Template node '{node.id}' received {len(inputs)} inputs; processing the latest entry", + node_id=node.id, + details={"input_count": len(inputs)}, + ) + + # Create sandboxed Jinja2 environment + env = SandboxedEnvironment( + autoescape=False, + undefined=StrictUndefined, # Strict mode - fail on undefined variables + ) + + # Register custom filters + env.filters["fromjson"] = _fromjson_filter + env.filters["tojson"] = _tojson_filter + + # Compile template + try: + template = env.from_string(config.template) + except TemplateSyntaxError as exc: + error_msg = f"Invalid template syntax in node '{node.id}': {exc}" + self.log_manager.error( + error_msg, node_id=node.id, details={"error": str(exc)} + ) + raise TemplateRenderError(error_msg) from exc + + # Build template context + template_context = { + "input": input_text, + "environment": getattr(self.context, "environment", {}), + } + + # Render template + try: + output = template.render(template_context) + except UndefinedError as exc: + available_keys = ", ".join(sorted(template_context.keys())) + error_msg = f"Undefined variable in template for node '{node.id}': {exc}. Available context keys: {available_keys}" + self.log_manager.error( + error_msg, + node_id=node.id, + details={"error": str(exc), "available_keys": available_keys}, + ) + raise TemplateRenderError(error_msg) from exc + except TemplateRenderError: + raise + except Exception as exc: + error_msg = f"Template rendering failed for node '{node.id}': {exc}" + self.log_manager.error( + error_msg, node_id=node.id, details={"error": str(exc)} + ) + raise TemplateRenderError(error_msg) from exc + + # Return new message with rendered output + return [ + self._build_message( + role=MessageRole.USER, + content=output, + source=node.id, + preserve_role=False, + ) + ] diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 65899b3f0..62ee99763 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -1,6 +1,28 @@ +# =================================================================== +# TEMPLATE DEMONSTRATION WORKFLOW +# =================================================================== +# This workflow demonstrates TWO ways to use Jinja2 templates: +# +# 1. EDGE TEMPLATE PROCESSORS (lines 150-227) +# - Transform data BETWEEN nodes on edges +# - Output appears in the TARGET node's input +# - Use case: Data transformation, format conversion +# - Examples: PatientDataGenerator → AdmissionReport +# +# 2. TEMPLATE NODES (lines 144-228) +# - Format data WITHIN node execution +# - Output appears in node's own logs +# - Use case: Formatted reports, summaries, dashboards +# - Examples: DischargeSummaryFormatter, MetricsDashboard +# +# Key Difference: +# - Edge processors transform data in transit +# - Template nodes are formatting destinations +# =================================================================== + graph: id: template_demo - description: Template edge processor demonstration with practical examples + description: Template edge processor and template node demonstration with practical examples log_level: INFO is_majority_voting: false nodes: @@ -138,7 +160,109 @@ graph: context_window: 0 log_output: true name: Status Generator + + # =================================================================== + # TEMPLATE NODES - Format data within node execution (not on edges) + # =================================================================== + + - id: DischargeDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient discharge data. Output ONLY valid JSON: + { + "patient_name": "Emily Chen", + "age": 75, + "admission_date": "2024-01-20", + "discharge_date": "2024-01-25", + "diagnosis": "Pneumonia", + "treatment": "Antibiotics and respiratory therapy", + "medications": ["Azithromycin 250mg", "Albuterol inhaler"], + "follow_up": "Visit pulmonologist in 1 week" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient discharge data + context_window: 0 + log_output: true + name: Discharge Data Generator + + - id: DischargeSummaryFormatter + type: template + config: + template: | + ============================================== + PATIENT DISCHARGE SUMMARY + ============================================== + {% set patient = input | fromjson %} + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + + Admission & Discharge: + - Admitted: {{ patient.admission_date }} + - Discharged: {{ patient.discharge_date }} + + Medical Details: + - Diagnosis: {{ patient.diagnosis }} + - Treatment: {{ patient.treatment }} + + Medications Prescribed: + {% for med in patient.medications %} + {{ loop.index }}. {{ med }} + {% endfor %} + + Follow-up Care: + {{ patient.follow_up }} + + {% if patient.age > 65 %} + ⚠️ Special Note: Elderly patient - ensure caregiver support. + {% endif %} + ============================================== + description: Formats patient discharge summary using template node + context_window: 0 + log_output: true + name: Discharge Summary Formatter + + - id: HospitalMetrics + type: literal + config: + content: "Today: 45 admissions, 38 discharges, 12 in ER" + role: user + description: Simple hospital metrics + context_window: 0 + log_output: true + name: Hospital Metrics + + - id: MetricsDashboard + type: template + config: + template: | + ╔════════════════════════════════════════╗ + ║ HOSPITAL METRICS DASHBOARD ║ + ╚════════════════════════════════════════╝ + + 📊 {{ input | upper }} + + Status: ALL SYSTEMS OPERATIONAL ✓ + Last Updated: {{ environment.run_id | default('N/A') }} + description: Formats metrics into dashboard using template node + context_window: 0 + log_output: true + name: Metrics Dashboard edges: + # =================================================================== + # EDGE TEMPLATE PROCESSORS - Transform data on edges between nodes + # =================================================================== + - from: PatientDataGenerator to: AdmissionReport trigger: true @@ -225,6 +349,32 @@ graph: System Status: {{ input }} Emergency Level: {{ emergency_level | default('None') }} Timestamp: {{ timestamp | default('Not specified') }} + + # =================================================================== + # TEMPLATE NODE EDGES - Connect to template nodes (no processor needed) + # =================================================================== + + - from: DischargeDataGenerator + to: DischargeSummaryFormatter + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: HospitalMetrics + to: MetricsDashboard + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + # Flow to final aggregator - from: AdmissionReport to: DailySummary trigger: true @@ -252,12 +402,34 @@ graph: clear_context: false clear_kept_context: false processor: null + + - from: DischargeSummaryFormatter + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: MetricsDashboard + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null memory: [] initial_instruction: '' start: - PatientDataGenerator - TriageDataGenerator - StatusGenerator + - DischargeDataGenerator + - HospitalMetrics end: - DailySummary version: 0.0.0 From 7255661967fd28b49b0abc5fd24a8a384368b3a1 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:35:17 +0700 Subject: [PATCH 10/42] fix: call text_content() method in template node executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix template node error where text_content was accessed as a property instead of being called as a method, causing JSON decode error. Error was: 'the JSON object must be str, bytes or bytearray, not method' Changed: latest_input.text_content → latest_input.text_content() --- runtime/node/executor/template_executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/node/executor/template_executor.py b/runtime/node/executor/template_executor.py index a1e9391a6..9abd0b2a5 100644 --- a/runtime/node/executor/template_executor.py +++ b/runtime/node/executor/template_executor.py @@ -57,7 +57,7 @@ def execute(self, node: Node, inputs: List[Message]) -> List[Message]: # Get latest input message (consistent with passthrough node behavior) latest_input = inputs[-1] - input_text = latest_input.text_content + input_text = latest_input.text_content() if len(inputs) > 1: self.log_manager.debug( From acf0efdc810b0698fc38fa4f8b5394dd474c9c2e Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:44:30 +0700 Subject: [PATCH 11/42] fix: replace StatusReport agent with template node Replace StatusReport agent node with a template node to prevent LLM from adding unwanted commentary to the status message. Issue: StatusReport agent was responding with COVID-19 advice instead of simply acknowledging the formatted status from the edge processor. Solution: Convert to template node that wraps the input in a clean acknowledgment format without AI interpretation. This demonstrates another template node use case: formatting acknowledgments/wrappers around already-formatted data. --- yaml_instance/template_demo.yaml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 62ee99763..28c15a3ac 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -103,19 +103,18 @@ graph: log_output: true name: Triage Report Handler - id: StatusReport - type: agent + type: template config: - name: ${MODEL_NAME} - provider: openai - role: You received a status update. Acknowledge it. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' + template: | + ============================================== + STATUS REPORT RECEIVED + ============================================== + + {{ input }} + + Report Status: ✓ LOGGED + ============================================== + description: Formats status acknowledgment context_window: 0 log_output: true name: Status Report Handler From f64ae206ab1dd4bb6f0bb00b1593e2f6c82e3152 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:57:29 +0700 Subject: [PATCH 12/42] refactor: remove decorative headers from template formats --- yaml_instance/template_demo.yaml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 28c15a3ac..79f13fb0c 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -106,14 +106,11 @@ graph: type: template config: template: | - ============================================== STATUS REPORT RECEIVED - ============================================== {{ input }} Report Status: ✓ LOGGED - ============================================== description: Formats status acknowledgment context_window: 0 log_output: true @@ -197,11 +194,10 @@ graph: type: template config: template: | - ============================================== - PATIENT DISCHARGE SUMMARY - ============================================== {% set patient = input | fromjson %} + PATIENT DISCHARGE SUMMARY + Patient Information: - Name: {{ patient.patient_name }} - Age: {{ patient.age }} years @@ -225,7 +221,6 @@ graph: {% if patient.age > 65 %} ⚠️ Special Note: Elderly patient - ensure caregiver support. {% endif %} - ============================================== description: Formats patient discharge summary using template node context_window: 0 log_output: true @@ -246,7 +241,7 @@ graph: config: template: | ╔════════════════════════════════════════╗ - ║ HOSPITAL METRICS DASHBOARD ║ + ║ HOSPITAL METRICS DASHBOARD ║ ╚════════════════════════════════════════╝ 📊 {{ input | upper }} @@ -274,9 +269,8 @@ graph: type: template config: template: | - ============================================== PATIENT ADMISSION REPORT - ============================================== + {% set patient = input | fromjson %} Patient Information: @@ -302,7 +296,6 @@ graph: {% if patient.age > 60 %} Note: Elderly patient - additional monitoring recommended. {% endif %} - ============================================== - from: TriageDataGenerator to: TriageReport trigger: true @@ -315,9 +308,8 @@ graph: type: template config: template: | - ============================================== TRIAGE QUEUE STATUS - ============================================== + {% set patients = input | fromjson %} Current Queue ({{ patients | length }} patients): @@ -332,7 +324,6 @@ graph: → WARNING: Extended wait time {% endif %} {% endfor %} - ============================================== - from: StatusGenerator to: StatusReport trigger: true From 8385dfadab04456fab4e06662a300ae423c49431 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:58:59 +0700 Subject: [PATCH 13/42] docs: update tasks checklist to reflect completed documentation --- .../add-template-formatter-node/tasks.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 openspec/changes/add-template-formatter-node/tasks.md diff --git a/openspec/changes/add-template-formatter-node/tasks.md b/openspec/changes/add-template-formatter-node/tasks.md new file mode 100644 index 000000000..6ac636ee8 --- /dev/null +++ b/openspec/changes/add-template-formatter-node/tasks.md @@ -0,0 +1,32 @@ +## 1. Implementation + +- [x] 1.1 Create template node configuration schema (`entity/configs/node/template.py`) +- [x] 1.2 Implement template node executor (`runtime/node/executor/template_executor.py`) +- [x] 1.3 Register template node in builtin nodes registry +- [x] 1.4 Add template node field specifications for frontend schema +- [x] 1.5 Write comprehensive unit tests for template node + +## 2. Validation + +- [x] 2.1 Test template rendering with JSON parsing (`fromjson` filter) +- [x] 2.2 Test template rendering with string operations (replace, trim, etc.) +- [x] 2.3 Test template rendering with conditional logic (if/else) +- [x] 2.4 Test template rendering with loops (for loops) +- [x] 2.5 Test error handling for invalid templates +- [x] 2.6 Test error handling for undefined variables +- [x] 2.7 Test log output at different levels (DEBUG, INFO) +- [x] 2.8 Validate YAML workflow configuration with check module + +## 3. Documentation + +- [x] 3.1 Add template node example to yaml_instance directory +- [ ] 3.2 Update workflow authoring guide with template node usage +- [x] 3.3 Document available Jinja2 filters and template syntax (TEMPLATE_NODE_GUIDE.md) +- [x] 3.4 Add comparison with literal node and edge processors (TEMPLATE_NODE_GUIDE.md) + +## 4. Integration + +- [ ] 4.1 Test template node in hospital simulation workflow +- [ ] 4.2 Verify template node output in workflow logs +- [ ] 4.3 Ensure template node works with dynamic execution (map mode) +- [ ] 4.4 Validate template node behavior with empty inputs From 79181baf6fed431e67744e859a0835f6b1edded4 Mon Sep 17 00:00:00 2001 From: laansdole Date: Tue, 10 Feb 2026 15:03:00 +0700 Subject: [PATCH 14/42] feat: demo YAML for node template --- yaml_instance/template_demo.yaml | 168 +++++++++++++++++++++++--- yaml_instance/template_node_demo.yaml | 142 ++++++++++++++++++++++ 2 files changed, 291 insertions(+), 19 deletions(-) create mode 100644 yaml_instance/template_node_demo.yaml diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 65899b3f0..6c3500ac0 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -1,6 +1,6 @@ graph: id: template_demo - description: Template edge processor demonstration with practical examples + description: Template edge processor and template node demonstration with practical examples log_level: INFO is_majority_voting: false nodes: @@ -81,19 +81,15 @@ graph: log_output: true name: Triage Report Handler - id: StatusReport - type: agent + type: template config: - name: ${MODEL_NAME} - provider: openai - role: You received a status update. Acknowledge it. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' + template: | + STATUS REPORT RECEIVED + + {{ input }} + + Report Status: ✓ LOGGED + description: Formats status acknowledgment context_window: 0 log_output: true name: Status Report Handler @@ -138,6 +134,96 @@ graph: context_window: 0 log_output: true name: Status Generator + + - id: DischargeDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient discharge data. Output ONLY valid JSON: + { + "patient_name": "Emily Chen", + "age": 75, + "admission_date": "2024-01-20", + "discharge_date": "2024-01-25", + "diagnosis": "Pneumonia", + "treatment": "Antibiotics and respiratory therapy", + "medications": ["Azithromycin 250mg", "Albuterol inhaler"], + "follow_up": "Visit pulmonologist in 1 week" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient discharge data + context_window: 0 + log_output: true + name: Discharge Data Generator + + - id: DischargeSummaryFormatter + type: template + config: + template: | + {% set patient = input | fromjson %} + + PATIENT DISCHARGE SUMMARY + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + + Admission & Discharge: + - Admitted: {{ patient.admission_date }} + - Discharged: {{ patient.discharge_date }} + + Medical Details: + - Diagnosis: {{ patient.diagnosis }} + - Treatment: {{ patient.treatment }} + + Medications Prescribed: + {% for med in patient.medications %} + {{ loop.index }}. {{ med }} + {% endfor %} + + Follow-up Care: + {{ patient.follow_up }} + + {% if patient.age > 65 %} + Special Note: Elderly patient - ensure caregiver support. + {% endif %} + description: Formats patient discharge summary using template node + context_window: 0 + log_output: true + name: Discharge Summary Formatter + + - id: HospitalMetrics + type: literal + config: + content: "Today: 45 admissions, 38 discharges, 12 in ER" + role: user + description: Simple hospital metrics + context_window: 0 + log_output: true + name: Hospital Metrics + + - id: MetricsDashboard + type: template + config: + template: | + HOSPITAL METRICS DASHBOARD + + {{ input | upper }} + + Status: ALL SYSTEMS OPERATIONAL ✓ + Last Updated: {{ environment.run_id | default('N/A') }} + description: Formats metrics into dashboard using template node + context_window: 0 + log_output: true + name: Metrics Dashboard edges: - from: PatientDataGenerator to: AdmissionReport @@ -151,9 +237,8 @@ graph: type: template config: template: | - ============================================== PATIENT ADMISSION REPORT - ============================================== + {% set patient = input | fromjson %} Patient Information: @@ -179,7 +264,6 @@ graph: {% if patient.age > 60 %} Note: Elderly patient - additional monitoring recommended. {% endif %} - ============================================== - from: TriageDataGenerator to: TriageReport trigger: true @@ -192,9 +276,8 @@ graph: type: template config: template: | - ============================================== TRIAGE QUEUE STATUS - ============================================== + {% set patients = input | fromjson %} Current Queue ({{ patients | length }} patients): @@ -209,7 +292,6 @@ graph: → WARNING: Extended wait time {% endif %} {% endfor %} - ============================================== - from: StatusGenerator to: StatusReport trigger: true @@ -225,6 +307,32 @@ graph: System Status: {{ input }} Emergency Level: {{ emergency_level | default('None') }} Timestamp: {{ timestamp | default('Not specified') }} + + # =================================================================== + # TEMPLATE NODE EDGES - Connect to template nodes (no processor needed) + # =================================================================== + + - from: DischargeDataGenerator + to: DischargeSummaryFormatter + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: HospitalMetrics + to: MetricsDashboard + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + # Flow to final aggregator - from: AdmissionReport to: DailySummary trigger: true @@ -252,12 +360,34 @@ graph: clear_context: false clear_kept_context: false processor: null + + - from: DischargeSummaryFormatter + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: MetricsDashboard + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null memory: [] initial_instruction: '' start: - PatientDataGenerator - TriageDataGenerator - StatusGenerator + - DischargeDataGenerator + - HospitalMetrics end: - DailySummary version: 0.0.0 diff --git a/yaml_instance/template_node_demo.yaml b/yaml_instance/template_node_demo.yaml new file mode 100644 index 000000000..ef7ef8746 --- /dev/null +++ b/yaml_instance/template_node_demo.yaml @@ -0,0 +1,142 @@ +graph: + id: template_node_demo + description: Template node demonstration with practical formatting examples + log_level: INFO + is_majority_voting: false + nodes: + - id: PatientDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient discharge data. Output ONLY valid JSON: + { + "patient_name": "Jane Doe", + "age": 72, + "admission_date": "2024-01-10", + "discharge_date": "2024-01-15", + "diagnosis": "Pneumonia", + "treatment": "Antibiotics and oxygen therapy", + "medications": ["Amoxicillin 500mg", "Ibuprofen 400mg"], + "follow_up": "Visit primary care physician in 2 weeks" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient discharge data + context_window: 0 + log_output: true + name: Patient Data Generator + - id: DischargeSummaryFormatter + type: template + config: + template: | + {% set patient = input | fromjson %} + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + + Admission & Discharge: + - Admitted: {{ patient.admission_date }} + - Discharged: {{ patient.discharge_date }} + + Medical Details: + - Diagnosis: {{ patient.diagnosis }} + - Treatment: {{ patient.treatment }} + + Medications Prescribed: + {% for med in patient.medications %} + {{ loop.index }}. {{ med }} + {% endfor %} + + Follow-up Care: + {{ patient.follow_up }} + + {% if patient.age > 65 %} + Special Note: Elderly patient - ensure caregiver support. + {% endif %} + description: Formats patient discharge summary + context_window: 0 + log_output: true + name: Discharge Summary Formatter + - id: SimpleTextGenerator + type: literal + config: + content: Hospital operations are running smoothly today. + role: user + description: Generates simple status text + context_window: 0 + log_output: true + name: Simple Text Generator + - id: StatusBanner + type: template + config: + template: | + {{ input | upper | center(38) }} + + Status at: {{ environment.run_id | default('N/A') }} + description: Creates formatted status banner + context_window: 0 + log_output: true + name: Status Banner Formatter + - id: ReportAggregator + type: passthrough + config: + only_last_message: false + description: Collects all formatted reports + context_window: 0 + log_output: true + name: Report Aggregator + edges: + - from: PatientDataGenerator + to: DischargeSummaryFormatter + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + - from: SimpleTextGenerator + to: StatusBanner + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + - from: DischargeSummaryFormatter + to: ReportAggregator + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + - from: StatusBanner + to: ReportAggregator + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + memory: [] + initial_instruction: '' + start: + - PatientDataGenerator + - SimpleTextGenerator + end: + - ReportAggregator +version: 0.0.0 +vars: + MODEL_NAME: qwen/qwen3-8b From 0ea37ec2a15c65356568b4b4ee88d254f802b118 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 20:55:00 +0700 Subject: [PATCH 15/42] feat: add template node for Jinja2-based formatting Add template node type that formats input messages using Jinja2 templates and emits rendered output directly to logs (without LLM interaction). Implementation: - entity/configs/node/template.py: Configuration schema with template field - runtime/node/executor/template_executor.py: Executor with sandboxed Jinja2 - runtime/node/builtin_nodes.py: Register template node type - yaml_instance/template_demo.yaml: Enhanced demo showing both edge processors and template nodes Features: - Sandboxed Jinja2 environment with StrictUndefined mode - Custom filters: fromjson, tojson (plus all standard Jinja2 filters) - Template context: input (latest message), environment (execution vars) - Error handling with detailed logging - Supports conditionals, loops, string operations Use case: - Format structured data (JSON) into human-readable reports - Create discharge summaries, dashboards, formatted logs - Deterministic formatting without AI reasoning --- entity/configs/node/template.py | 42 +++++ runtime/node/builtin_nodes.py | 21 ++- runtime/node/executor/template_executor.py | 124 +++++++++++++++ yaml_instance/template_demo.yaml | 174 ++++++++++++++++++++- 4 files changed, 354 insertions(+), 7 deletions(-) create mode 100644 entity/configs/node/template.py create mode 100644 runtime/node/executor/template_executor.py diff --git a/entity/configs/node/template.py b/entity/configs/node/template.py new file mode 100644 index 000000000..e3a7ee889 --- /dev/null +++ b/entity/configs/node/template.py @@ -0,0 +1,42 @@ +"""Configuration for template nodes.""" + +from dataclasses import dataclass +from typing import Mapping, Any + +from entity.configs.base import ( + BaseConfig, + ConfigError, + ConfigFieldSpec, + require_mapping, + require_str, +) + + +@dataclass +class TemplateNodeConfig(BaseConfig): + """Config describing the Jinja2 template used to format output.""" + + template: str = "" + + @classmethod + def from_dict(cls, data: Mapping[str, Any], *, path: str) -> "TemplateNodeConfig": + mapping = require_mapping(data, path) + template = require_str(mapping, "template", path) + if not template: + raise ConfigError("template cannot be empty", f"{path}.template") + + return cls(template=template, path=path) + + def validate(self) -> None: + if not self.template: + raise ConfigError("template cannot be empty", f"{self.path}.template") + + FIELD_SPECS = { + "template": ConfigFieldSpec( + name="template", + display_name="Jinja2 Template", + type_hint="text", + required=True, + description="Jinja2 template string for formatting output. Available context: {{ input }} (latest message content), {{ environment }} (execution environment variables).", + ), + } diff --git a/runtime/node/builtin_nodes.py b/runtime/node/builtin_nodes.py index 2a2311156..a3ef6ac01 100755 --- a/runtime/node/builtin_nodes.py +++ b/runtime/node/builtin_nodes.py @@ -12,6 +12,7 @@ from entity.configs.node.literal import LiteralNodeConfig from entity.configs.node.python_runner import PythonRunnerConfig from entity.configs.node.loop_counter import LoopCounterConfig +from entity.configs.node.template import TemplateNodeConfig from runtime.node.executor.agent_executor import AgentNodeExecutor from runtime.node.executor.human_executor import HumanNodeExecutor from runtime.node.executor.passthrough_executor import PassthroughNodeExecutor @@ -19,6 +20,7 @@ from runtime.node.executor.python_executor import PythonNodeExecutor from runtime.node.executor.subgraph_executor import SubgraphNodeExecutor from runtime.node.executor.loop_counter_executor import LoopCounterNodeExecutor +from runtime.node.executor.template_executor import TemplateNodeExecutor from runtime.node.registry import NodeCapabilities, register_node_type @@ -48,9 +50,10 @@ "subgraph", config_cls=SubgraphConfig, executor_cls=SubgraphNodeExecutor, - capabilities=NodeCapabilities( + capabilities=NodeCapabilities(), + executor_factory=lambda context, subgraphs=None: SubgraphNodeExecutor( + context, subgraphs or {} ), - executor_factory=lambda context, subgraphs=None: SubgraphNodeExecutor(context, subgraphs or {}), summary="Embeds (through file path or inline config) and runs another named subgraph within the current workflow", ) @@ -69,8 +72,7 @@ "passthrough", config_cls=PassthroughConfig, executor_cls=PassthroughNodeExecutor, - capabilities=NodeCapabilities( - ), + capabilities=NodeCapabilities(), summary="Forwards prior node output downstream without modification", ) @@ -78,8 +80,7 @@ "literal", config_cls=LiteralNodeConfig, executor_cls=LiteralNodeExecutor, - capabilities=NodeCapabilities( - ), + capabilities=NodeCapabilities(), summary="Emits the configured text message every time it is triggered", ) @@ -91,6 +92,14 @@ summary="Blocks downstream edges until the configured iteration limit is reached, then emits a message to release the loop.", ) +register_node_type( + "template", + config_cls=TemplateNodeConfig, + executor_cls=TemplateNodeExecutor, + capabilities=NodeCapabilities(), + summary="Formats input messages using Jinja2 templates and emits the rendered output", +) + # Register subgraph source types (file-based and inline config) register_subgraph_source( "config", diff --git a/runtime/node/executor/template_executor.py b/runtime/node/executor/template_executor.py new file mode 100644 index 000000000..a1e9391a6 --- /dev/null +++ b/runtime/node/executor/template_executor.py @@ -0,0 +1,124 @@ +"""Template node executor.""" + +import json +from typing import List, Any + +from jinja2 import TemplateSyntaxError, UndefinedError, StrictUndefined +from jinja2.sandbox import SandboxedEnvironment + +from entity.configs import Node +from entity.configs.node.template import TemplateNodeConfig +from entity.messages import Message, MessageRole +from runtime.node.executor.base import NodeExecutor + + +class TemplateRenderError(Exception): + """Raised when template rendering fails.""" + + pass + + +def _fromjson_filter(value: str) -> Any: + """Parse JSON string into Python object.""" + try: + return json.loads(value) + except json.JSONDecodeError as exc: + raise TemplateRenderError(f"JSON decode error: {exc}") from exc + + +def _tojson_filter(value: Any) -> str: + """Serialize Python object to JSON string.""" + try: + return json.dumps(value, ensure_ascii=False) + except (TypeError, ValueError) as exc: + raise TemplateRenderError(f"JSON encode error: {exc}") from exc + + +class TemplateNodeExecutor(NodeExecutor): + """Format input messages using Jinja2 templates and emit the result.""" + + def execute(self, node: Node, inputs: List[Message]) -> List[Message]: + if node.node_type != "template": + raise ValueError(f"Node {node.id} is not a template node") + + config = node.as_config(TemplateNodeConfig) + if config is None: + raise ValueError(f"Node {node.id} missing template configuration") + + self._ensure_not_cancelled() + + # Handle empty inputs - return empty message + if not inputs: + warning_msg = f"Template node '{node.id}' triggered without inputs" + self.log_manager.warning( + warning_msg, node_id=node.id, details={"input_count": 0} + ) + return [Message(content="", role=MessageRole.USER)] + + # Get latest input message (consistent with passthrough node behavior) + latest_input = inputs[-1] + input_text = latest_input.text_content + + if len(inputs) > 1: + self.log_manager.debug( + f"Template node '{node.id}' received {len(inputs)} inputs; processing the latest entry", + node_id=node.id, + details={"input_count": len(inputs)}, + ) + + # Create sandboxed Jinja2 environment + env = SandboxedEnvironment( + autoescape=False, + undefined=StrictUndefined, # Strict mode - fail on undefined variables + ) + + # Register custom filters + env.filters["fromjson"] = _fromjson_filter + env.filters["tojson"] = _tojson_filter + + # Compile template + try: + template = env.from_string(config.template) + except TemplateSyntaxError as exc: + error_msg = f"Invalid template syntax in node '{node.id}': {exc}" + self.log_manager.error( + error_msg, node_id=node.id, details={"error": str(exc)} + ) + raise TemplateRenderError(error_msg) from exc + + # Build template context + template_context = { + "input": input_text, + "environment": getattr(self.context, "environment", {}), + } + + # Render template + try: + output = template.render(template_context) + except UndefinedError as exc: + available_keys = ", ".join(sorted(template_context.keys())) + error_msg = f"Undefined variable in template for node '{node.id}': {exc}. Available context keys: {available_keys}" + self.log_manager.error( + error_msg, + node_id=node.id, + details={"error": str(exc), "available_keys": available_keys}, + ) + raise TemplateRenderError(error_msg) from exc + except TemplateRenderError: + raise + except Exception as exc: + error_msg = f"Template rendering failed for node '{node.id}': {exc}" + self.log_manager.error( + error_msg, node_id=node.id, details={"error": str(exc)} + ) + raise TemplateRenderError(error_msg) from exc + + # Return new message with rendered output + return [ + self._build_message( + role=MessageRole.USER, + content=output, + source=node.id, + preserve_role=False, + ) + ] diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 65899b3f0..62ee99763 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -1,6 +1,28 @@ +# =================================================================== +# TEMPLATE DEMONSTRATION WORKFLOW +# =================================================================== +# This workflow demonstrates TWO ways to use Jinja2 templates: +# +# 1. EDGE TEMPLATE PROCESSORS (lines 150-227) +# - Transform data BETWEEN nodes on edges +# - Output appears in the TARGET node's input +# - Use case: Data transformation, format conversion +# - Examples: PatientDataGenerator → AdmissionReport +# +# 2. TEMPLATE NODES (lines 144-228) +# - Format data WITHIN node execution +# - Output appears in node's own logs +# - Use case: Formatted reports, summaries, dashboards +# - Examples: DischargeSummaryFormatter, MetricsDashboard +# +# Key Difference: +# - Edge processors transform data in transit +# - Template nodes are formatting destinations +# =================================================================== + graph: id: template_demo - description: Template edge processor demonstration with practical examples + description: Template edge processor and template node demonstration with practical examples log_level: INFO is_majority_voting: false nodes: @@ -138,7 +160,109 @@ graph: context_window: 0 log_output: true name: Status Generator + + # =================================================================== + # TEMPLATE NODES - Format data within node execution (not on edges) + # =================================================================== + + - id: DischargeDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient discharge data. Output ONLY valid JSON: + { + "patient_name": "Emily Chen", + "age": 75, + "admission_date": "2024-01-20", + "discharge_date": "2024-01-25", + "diagnosis": "Pneumonia", + "treatment": "Antibiotics and respiratory therapy", + "medications": ["Azithromycin 250mg", "Albuterol inhaler"], + "follow_up": "Visit pulmonologist in 1 week" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient discharge data + context_window: 0 + log_output: true + name: Discharge Data Generator + + - id: DischargeSummaryFormatter + type: template + config: + template: | + ============================================== + PATIENT DISCHARGE SUMMARY + ============================================== + {% set patient = input | fromjson %} + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + + Admission & Discharge: + - Admitted: {{ patient.admission_date }} + - Discharged: {{ patient.discharge_date }} + + Medical Details: + - Diagnosis: {{ patient.diagnosis }} + - Treatment: {{ patient.treatment }} + + Medications Prescribed: + {% for med in patient.medications %} + {{ loop.index }}. {{ med }} + {% endfor %} + + Follow-up Care: + {{ patient.follow_up }} + + {% if patient.age > 65 %} + ⚠️ Special Note: Elderly patient - ensure caregiver support. + {% endif %} + ============================================== + description: Formats patient discharge summary using template node + context_window: 0 + log_output: true + name: Discharge Summary Formatter + + - id: HospitalMetrics + type: literal + config: + content: "Today: 45 admissions, 38 discharges, 12 in ER" + role: user + description: Simple hospital metrics + context_window: 0 + log_output: true + name: Hospital Metrics + + - id: MetricsDashboard + type: template + config: + template: | + ╔════════════════════════════════════════╗ + ║ HOSPITAL METRICS DASHBOARD ║ + ╚════════════════════════════════════════╝ + + 📊 {{ input | upper }} + + Status: ALL SYSTEMS OPERATIONAL ✓ + Last Updated: {{ environment.run_id | default('N/A') }} + description: Formats metrics into dashboard using template node + context_window: 0 + log_output: true + name: Metrics Dashboard edges: + # =================================================================== + # EDGE TEMPLATE PROCESSORS - Transform data on edges between nodes + # =================================================================== + - from: PatientDataGenerator to: AdmissionReport trigger: true @@ -225,6 +349,32 @@ graph: System Status: {{ input }} Emergency Level: {{ emergency_level | default('None') }} Timestamp: {{ timestamp | default('Not specified') }} + + # =================================================================== + # TEMPLATE NODE EDGES - Connect to template nodes (no processor needed) + # =================================================================== + + - from: DischargeDataGenerator + to: DischargeSummaryFormatter + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: HospitalMetrics + to: MetricsDashboard + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + # Flow to final aggregator - from: AdmissionReport to: DailySummary trigger: true @@ -252,12 +402,34 @@ graph: clear_context: false clear_kept_context: false processor: null + + - from: DischargeSummaryFormatter + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null + + - from: MetricsDashboard + to: DailySummary + trigger: true + condition: 'true' + carry_data: true + keep_message: false + clear_context: false + clear_kept_context: false + processor: null memory: [] initial_instruction: '' start: - PatientDataGenerator - TriageDataGenerator - StatusGenerator + - DischargeDataGenerator + - HospitalMetrics end: - DailySummary version: 0.0.0 From bda859d6da66bb5df2debc1bdd117331b69bf17b Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:35:17 +0700 Subject: [PATCH 16/42] fix: call text_content() method in template node executor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix template node error where text_content was accessed as a property instead of being called as a method, causing JSON decode error. Error was: 'the JSON object must be str, bytes or bytearray, not method' Changed: latest_input.text_content → latest_input.text_content() --- runtime/node/executor/template_executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/node/executor/template_executor.py b/runtime/node/executor/template_executor.py index a1e9391a6..9abd0b2a5 100644 --- a/runtime/node/executor/template_executor.py +++ b/runtime/node/executor/template_executor.py @@ -57,7 +57,7 @@ def execute(self, node: Node, inputs: List[Message]) -> List[Message]: # Get latest input message (consistent with passthrough node behavior) latest_input = inputs[-1] - input_text = latest_input.text_content + input_text = latest_input.text_content() if len(inputs) > 1: self.log_manager.debug( From 4af4c19a30e20a7e6d995cf26c10fe549755d293 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:44:30 +0700 Subject: [PATCH 17/42] fix: replace StatusReport agent with template node Replace StatusReport agent node with a template node to prevent LLM from adding unwanted commentary to the status message. Issue: StatusReport agent was responding with COVID-19 advice instead of simply acknowledging the formatted status from the edge processor. Solution: Convert to template node that wraps the input in a clean acknowledgment format without AI interpretation. This demonstrates another template node use case: formatting acknowledgments/wrappers around already-formatted data. --- yaml_instance/template_demo.yaml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 62ee99763..28c15a3ac 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -103,19 +103,18 @@ graph: log_output: true name: Triage Report Handler - id: StatusReport - type: agent + type: template config: - name: ${MODEL_NAME} - provider: openai - role: You received a status update. Acknowledge it. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' + template: | + ============================================== + STATUS REPORT RECEIVED + ============================================== + + {{ input }} + + Report Status: ✓ LOGGED + ============================================== + description: Formats status acknowledgment context_window: 0 log_output: true name: Status Report Handler From 2f3f3ef934c55209b303585ec60cc7301ca937ab Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:57:29 +0700 Subject: [PATCH 18/42] refactor: remove decorative headers from template formats --- yaml_instance/template_demo.yaml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 28c15a3ac..79f13fb0c 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -106,14 +106,11 @@ graph: type: template config: template: | - ============================================== STATUS REPORT RECEIVED - ============================================== {{ input }} Report Status: ✓ LOGGED - ============================================== description: Formats status acknowledgment context_window: 0 log_output: true @@ -197,11 +194,10 @@ graph: type: template config: template: | - ============================================== - PATIENT DISCHARGE SUMMARY - ============================================== {% set patient = input | fromjson %} + PATIENT DISCHARGE SUMMARY + Patient Information: - Name: {{ patient.patient_name }} - Age: {{ patient.age }} years @@ -225,7 +221,6 @@ graph: {% if patient.age > 65 %} ⚠️ Special Note: Elderly patient - ensure caregiver support. {% endif %} - ============================================== description: Formats patient discharge summary using template node context_window: 0 log_output: true @@ -246,7 +241,7 @@ graph: config: template: | ╔════════════════════════════════════════╗ - ║ HOSPITAL METRICS DASHBOARD ║ + ║ HOSPITAL METRICS DASHBOARD ║ ╚════════════════════════════════════════╝ 📊 {{ input | upper }} @@ -274,9 +269,8 @@ graph: type: template config: template: | - ============================================== PATIENT ADMISSION REPORT - ============================================== + {% set patient = input | fromjson %} Patient Information: @@ -302,7 +296,6 @@ graph: {% if patient.age > 60 %} Note: Elderly patient - additional monitoring recommended. {% endif %} - ============================================== - from: TriageDataGenerator to: TriageReport trigger: true @@ -315,9 +308,8 @@ graph: type: template config: template: | - ============================================== TRIAGE QUEUE STATUS - ============================================== + {% set patients = input | fromjson %} Current Queue ({{ patients | length }} patients): @@ -332,7 +324,6 @@ graph: → WARNING: Extended wait time {% endif %} {% endfor %} - ============================================== - from: StatusGenerator to: StatusReport trigger: true From 1488d926a1f9995a0189c584dd7ab37d12184461 Mon Sep 17 00:00:00 2001 From: laansdole Date: Mon, 9 Feb 2026 21:58:59 +0700 Subject: [PATCH 19/42] docs: update tasks checklist to reflect completed documentation --- .../add-template-formatter-node/tasks.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 openspec/changes/add-template-formatter-node/tasks.md diff --git a/openspec/changes/add-template-formatter-node/tasks.md b/openspec/changes/add-template-formatter-node/tasks.md new file mode 100644 index 000000000..6ac636ee8 --- /dev/null +++ b/openspec/changes/add-template-formatter-node/tasks.md @@ -0,0 +1,32 @@ +## 1. Implementation + +- [x] 1.1 Create template node configuration schema (`entity/configs/node/template.py`) +- [x] 1.2 Implement template node executor (`runtime/node/executor/template_executor.py`) +- [x] 1.3 Register template node in builtin nodes registry +- [x] 1.4 Add template node field specifications for frontend schema +- [x] 1.5 Write comprehensive unit tests for template node + +## 2. Validation + +- [x] 2.1 Test template rendering with JSON parsing (`fromjson` filter) +- [x] 2.2 Test template rendering with string operations (replace, trim, etc.) +- [x] 2.3 Test template rendering with conditional logic (if/else) +- [x] 2.4 Test template rendering with loops (for loops) +- [x] 2.5 Test error handling for invalid templates +- [x] 2.6 Test error handling for undefined variables +- [x] 2.7 Test log output at different levels (DEBUG, INFO) +- [x] 2.8 Validate YAML workflow configuration with check module + +## 3. Documentation + +- [x] 3.1 Add template node example to yaml_instance directory +- [ ] 3.2 Update workflow authoring guide with template node usage +- [x] 3.3 Document available Jinja2 filters and template syntax (TEMPLATE_NODE_GUIDE.md) +- [x] 3.4 Add comparison with literal node and edge processors (TEMPLATE_NODE_GUIDE.md) + +## 4. Integration + +- [ ] 4.1 Test template node in hospital simulation workflow +- [ ] 4.2 Verify template node output in workflow logs +- [ ] 4.3 Ensure template node works with dynamic execution (map mode) +- [ ] 4.4 Validate template node behavior with empty inputs From 906a17bbbeabe980f6ad097e40168e152e5e12bf Mon Sep 17 00:00:00 2001 From: laansdole Date: Tue, 10 Feb 2026 16:53:47 +0700 Subject: [PATCH 20/42] chores: refactor template demo --- yaml_instance/template_demo.yaml | 132 ++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 39 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index 79f13fb0c..23e5806eb 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -1,25 +1,3 @@ -# =================================================================== -# TEMPLATE DEMONSTRATION WORKFLOW -# =================================================================== -# This workflow demonstrates TWO ways to use Jinja2 templates: -# -# 1. EDGE TEMPLATE PROCESSORS (lines 150-227) -# - Transform data BETWEEN nodes on edges -# - Output appears in the TARGET node's input -# - Use case: Data transformation, format conversion -# - Examples: PatientDataGenerator → AdmissionReport -# -# 2. TEMPLATE NODES (lines 144-228) -# - Format data WITHIN node execution -# - Output appears in node's own logs -# - Use case: Formatted reports, summaries, dashboards -# - Examples: DischargeSummaryFormatter, MetricsDashboard -# -# Key Difference: -# - Edge processors transform data in transit -# - Template nodes are formatting destinations -# =================================================================== - graph: id: template_demo description: Template edge processor and template node demonstration with practical examples @@ -157,9 +135,95 @@ graph: log_output: true name: Status Generator - # =================================================================== - # TEMPLATE NODES - Format data within node execution (not on edges) - # =================================================================== + - id: DischargeDataGenerator + type: agent + config: + name: ${MODEL_NAME} + provider: openai + role: | + Generate a JSON object with patient discharge data. Output ONLY valid JSON: + { + "patient_name": "Emily Chen", + "age": 75, + "admission_date": "2024-01-20", + "discharge_date": "2024-01-25", + "diagnosis": "Pneumonia", + "treatment": "Antibiotics and respiratory therapy", + "medications": ["Azithromycin 250mg", "Albuterol inhaler"], + "follow_up": "Visit pulmonologist in 1 week" + } + base_url: ${BASE_URL} + api_key: ${API_KEY} + params: {} + tooling: null + thinking: null + memories: [] + retry: null + description: Generates patient discharge data + context_window: 0 + log_output: true + name: Discharge Data Generator + + - id: DischargeSummaryFormatter + type: template + config: + template: | + {% set patient = input | fromjson %} + + PATIENT DISCHARGE SUMMARY + + Patient Information: + - Name: {{ patient.patient_name }} + - Age: {{ patient.age }} years + + Admission & Discharge: + - Admitted: {{ patient.admission_date }} + - Discharged: {{ patient.discharge_date }} + + Medical Details: + - Diagnosis: {{ patient.diagnosis }} + - Treatment: {{ patient.treatment }} + + Medications Prescribed: + {% for med in patient.medications %} + {{ loop.index }}. {{ med }} + {% endfor %} + + Follow-up Care: + {{ patient.follow_up }} + + {% if patient.age > 65 %} + Special Note: Elderly patient - ensure caregiver support. + {% endif %} + description: Formats patient discharge summary using template node + context_window: 0 + log_output: true + name: Discharge Summary Formatter + + - id: HospitalMetrics + type: literal + config: + content: "Today: 45 admissions, 38 discharges, 12 in ER" + role: user + description: Simple hospital metrics + context_window: 0 + log_output: true + name: Hospital Metrics + + - id: MetricsDashboard + type: template + config: + template: | + HOSPITAL METRICS DASHBOARD + + {{ input | upper }} + + Status: ALL SYSTEMS OPERATIONAL ✓ + Last Updated: {{ environment.run_id | default('N/A') }} + description: Formats metrics into dashboard using template node + context_window: 0 + log_output: true + name: Metrics Dashboard - id: DischargeDataGenerator type: agent @@ -219,7 +283,7 @@ graph: {{ patient.follow_up }} {% if patient.age > 65 %} - ⚠️ Special Note: Elderly patient - ensure caregiver support. + Special Note: Elderly patient - ensure caregiver support. {% endif %} description: Formats patient discharge summary using template node context_window: 0 @@ -240,11 +304,9 @@ graph: type: template config: template: | - ╔════════════════════════════════════════╗ - ║ HOSPITAL METRICS DASHBOARD ║ - ╚════════════════════════════════════════╝ + HOSPITAL METRICS DASHBOARD - 📊 {{ input | upper }} + {{ input | upper }} Status: ALL SYSTEMS OPERATIONAL ✓ Last Updated: {{ environment.run_id | default('N/A') }} @@ -253,10 +315,6 @@ graph: log_output: true name: Metrics Dashboard edges: - # =================================================================== - # EDGE TEMPLATE PROCESSORS - Transform data on edges between nodes - # =================================================================== - - from: PatientDataGenerator to: AdmissionReport trigger: true @@ -340,10 +398,6 @@ graph: Emergency Level: {{ emergency_level | default('None') }} Timestamp: {{ timestamp | default('Not specified') }} - # =================================================================== - # TEMPLATE NODE EDGES - Connect to template nodes (no processor needed) - # =================================================================== - - from: DischargeDataGenerator to: DischargeSummaryFormatter trigger: true @@ -424,4 +478,4 @@ graph: - DailySummary version: 0.0.0 vars: - MODEL_NAME: qwen/qwen3-8b + MODEL_NAME: qwen/qwen3-8b \ No newline at end of file From cd65c680640c0e48c5902a42444c630137b0809d Mon Sep 17 00:00:00 2001 From: laansdole Date: Tue, 10 Feb 2026 21:03:29 +0700 Subject: [PATCH 21/42] fix: template demo YAML file --- yaml_instance/template_demo.yaml | 116 ------------------------------- 1 file changed, 116 deletions(-) diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml index b97ba8023..e70b4af82 100644 --- a/yaml_instance/template_demo.yaml +++ b/yaml_instance/template_demo.yaml @@ -1,25 +1,3 @@ -# =================================================================== -# TEMPLATE DEMONSTRATION WORKFLOW -# =================================================================== -# This workflow demonstrates TWO ways to use Jinja2 templates: -# -# 1. EDGE TEMPLATE PROCESSORS (lines 150-227) -# - Transform data BETWEEN nodes on edges -# - Output appears in the TARGET node's input -# - Use case: Data transformation, format conversion -# - Examples: PatientDataGenerator → AdmissionReport -# -# 2. TEMPLATE NODES (lines 144-228) -# - Format data WITHIN node execution -# - Output appears in node's own logs -# - Use case: Formatted reports, summaries, dashboards -# - Examples: DischargeSummaryFormatter, MetricsDashboard -# -# Key Difference: -# - Edge processors transform data in transit -# - Template nodes are formatting destinations -# =================================================================== - graph: id: template_demo description: Template edge processor and template node demonstration with practical examples @@ -246,101 +224,7 @@ graph: context_window: 0 log_output: true name: Metrics Dashboard - - - id: DischargeDataGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: | - Generate a JSON object with patient discharge data. Output ONLY valid JSON: - { - "patient_name": "Emily Chen", - "age": 75, - "admission_date": "2024-01-20", - "discharge_date": "2024-01-25", - "diagnosis": "Pneumonia", - "treatment": "Antibiotics and respiratory therapy", - "medications": ["Azithromycin 250mg", "Albuterol inhaler"], - "follow_up": "Visit pulmonologist in 1 week" - } - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: Generates patient discharge data - context_window: 0 - log_output: true - name: Discharge Data Generator - - - id: DischargeSummaryFormatter - type: template - config: - template: | - {% set patient = input | fromjson %} - - PATIENT DISCHARGE SUMMARY - - Patient Information: - - Name: {{ patient.patient_name }} - - Age: {{ patient.age }} years - - Admission & Discharge: - - Admitted: {{ patient.admission_date }} - - Discharged: {{ patient.discharge_date }} - - Medical Details: - - Diagnosis: {{ patient.diagnosis }} - - Treatment: {{ patient.treatment }} - - Medications Prescribed: - {% for med in patient.medications %} - {{ loop.index }}. {{ med }} - {% endfor %} - - Follow-up Care: - {{ patient.follow_up }} - - {% if patient.age > 65 %} - Special Note: Elderly patient - ensure caregiver support. - {% endif %} - description: Formats patient discharge summary using template node - context_window: 0 - log_output: true - name: Discharge Summary Formatter - - - id: HospitalMetrics - type: literal - config: - content: "Today: 45 admissions, 38 discharges, 12 in ER" - role: user - description: Simple hospital metrics - context_window: 0 - log_output: true - name: Hospital Metrics - - - id: MetricsDashboard - type: template - config: - template: | - HOSPITAL METRICS DASHBOARD - - {{ input | upper }} - - Status: ALL SYSTEMS OPERATIONAL ✓ - Last Updated: {{ environment.run_id | default('N/A') }} - description: Formats metrics into dashboard using template node - context_window: 0 - log_output: true - name: Metrics Dashboard edges: - # =================================================================== - # EDGE TEMPLATE PROCESSORS - Transform data on edges between nodes - # =================================================================== - - from: PatientDataGenerator to: AdmissionReport trigger: true From 78b22228b8569e55eafc577e7e58ead76da1cc2f Mon Sep 17 00:00:00 2001 From: laansdole Date: Tue, 10 Feb 2026 21:27:35 +0700 Subject: [PATCH 22/42] chores: remove template demo --- yaml_instance/template_demo.yaml | 481 ------------------------------- 1 file changed, 481 deletions(-) delete mode 100644 yaml_instance/template_demo.yaml diff --git a/yaml_instance/template_demo.yaml b/yaml_instance/template_demo.yaml deleted file mode 100644 index 23e5806eb..000000000 --- a/yaml_instance/template_demo.yaml +++ /dev/null @@ -1,481 +0,0 @@ -graph: - id: template_demo - description: Template edge processor and template node demonstration with practical examples - log_level: INFO - is_majority_voting: false - nodes: - - id: PatientDataGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: | - Generate a JSON object with patient data. Output ONLY valid JSON: - { - "patient_name": "John Smith", - "age": 68, - "condition": "Influenza", - "severity": "high", - "admission_date": "2024-01-15" - } - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: Generates patient admission data - context_window: 0 - log_output: true - name: Patient Data Generator - - id: AdmissionReport - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: You received a patient admission report. Acknowledge it. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' - context_window: 0 - log_output: true - name: Admission Report Handler - - id: DailySummary - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: You received multiple reports. Create a brief daily summary. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' - context_window: 0 - log_output: true - name: Daily Summary Generator - - id: TriageReport - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: You received a triage status report. Acknowledge it. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' - context_window: 0 - log_output: true - name: Triage Report Handler - - id: StatusReport - type: template - config: - template: | - STATUS REPORT RECEIVED - - {{ input }} - - Report Status: ✓ LOGGED - description: Formats status acknowledgment - context_window: 0 - log_output: true - name: Status Report Handler - - id: TriageDataGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: | - Generate a JSON array of 3 patients in triage: - [ - {"name": "Alice Johnson", "priority": "high", "wait_time": 5}, - {"name": "Bob Williams", "priority": "medium", "wait_time": 20}, - {"name": "Carol Davis", "priority": "low", "wait_time": 45} - ] - Output ONLY valid JSON. - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: Generates triage queue data - context_window: 0 - log_output: true - name: Triage Data Generator - - id: StatusGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: 'Output exactly: "Hospital operations normal"' - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: '' - context_window: 0 - log_output: true - name: Status Generator - - - id: DischargeDataGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: | - Generate a JSON object with patient discharge data. Output ONLY valid JSON: - { - "patient_name": "Emily Chen", - "age": 75, - "admission_date": "2024-01-20", - "discharge_date": "2024-01-25", - "diagnosis": "Pneumonia", - "treatment": "Antibiotics and respiratory therapy", - "medications": ["Azithromycin 250mg", "Albuterol inhaler"], - "follow_up": "Visit pulmonologist in 1 week" - } - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: Generates patient discharge data - context_window: 0 - log_output: true - name: Discharge Data Generator - - - id: DischargeSummaryFormatter - type: template - config: - template: | - {% set patient = input | fromjson %} - - PATIENT DISCHARGE SUMMARY - - Patient Information: - - Name: {{ patient.patient_name }} - - Age: {{ patient.age }} years - - Admission & Discharge: - - Admitted: {{ patient.admission_date }} - - Discharged: {{ patient.discharge_date }} - - Medical Details: - - Diagnosis: {{ patient.diagnosis }} - - Treatment: {{ patient.treatment }} - - Medications Prescribed: - {% for med in patient.medications %} - {{ loop.index }}. {{ med }} - {% endfor %} - - Follow-up Care: - {{ patient.follow_up }} - - {% if patient.age > 65 %} - Special Note: Elderly patient - ensure caregiver support. - {% endif %} - description: Formats patient discharge summary using template node - context_window: 0 - log_output: true - name: Discharge Summary Formatter - - - id: HospitalMetrics - type: literal - config: - content: "Today: 45 admissions, 38 discharges, 12 in ER" - role: user - description: Simple hospital metrics - context_window: 0 - log_output: true - name: Hospital Metrics - - - id: MetricsDashboard - type: template - config: - template: | - HOSPITAL METRICS DASHBOARD - - {{ input | upper }} - - Status: ALL SYSTEMS OPERATIONAL ✓ - Last Updated: {{ environment.run_id | default('N/A') }} - description: Formats metrics into dashboard using template node - context_window: 0 - log_output: true - name: Metrics Dashboard - - - id: DischargeDataGenerator - type: agent - config: - name: ${MODEL_NAME} - provider: openai - role: | - Generate a JSON object with patient discharge data. Output ONLY valid JSON: - { - "patient_name": "Emily Chen", - "age": 75, - "admission_date": "2024-01-20", - "discharge_date": "2024-01-25", - "diagnosis": "Pneumonia", - "treatment": "Antibiotics and respiratory therapy", - "medications": ["Azithromycin 250mg", "Albuterol inhaler"], - "follow_up": "Visit pulmonologist in 1 week" - } - base_url: ${BASE_URL} - api_key: ${API_KEY} - params: {} - tooling: null - thinking: null - memories: [] - retry: null - description: Generates patient discharge data - context_window: 0 - log_output: true - name: Discharge Data Generator - - - id: DischargeSummaryFormatter - type: template - config: - template: | - {% set patient = input | fromjson %} - - PATIENT DISCHARGE SUMMARY - - Patient Information: - - Name: {{ patient.patient_name }} - - Age: {{ patient.age }} years - - Admission & Discharge: - - Admitted: {{ patient.admission_date }} - - Discharged: {{ patient.discharge_date }} - - Medical Details: - - Diagnosis: {{ patient.diagnosis }} - - Treatment: {{ patient.treatment }} - - Medications Prescribed: - {% for med in patient.medications %} - {{ loop.index }}. {{ med }} - {% endfor %} - - Follow-up Care: - {{ patient.follow_up }} - - {% if patient.age > 65 %} - Special Note: Elderly patient - ensure caregiver support. - {% endif %} - description: Formats patient discharge summary using template node - context_window: 0 - log_output: true - name: Discharge Summary Formatter - - - id: HospitalMetrics - type: literal - config: - content: "Today: 45 admissions, 38 discharges, 12 in ER" - role: user - description: Simple hospital metrics - context_window: 0 - log_output: true - name: Hospital Metrics - - - id: MetricsDashboard - type: template - config: - template: | - HOSPITAL METRICS DASHBOARD - - {{ input | upper }} - - Status: ALL SYSTEMS OPERATIONAL ✓ - Last Updated: {{ environment.run_id | default('N/A') }} - description: Formats metrics into dashboard using template node - context_window: 0 - log_output: true - name: Metrics Dashboard - edges: - - from: PatientDataGenerator - to: AdmissionReport - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: - type: template - config: - template: | - PATIENT ADMISSION REPORT - - {% set patient = input | fromjson %} - - Patient Information: - - Name: {{ patient.patient_name }} - - Age: {{ patient.age }} years - - Condition: {{ patient.condition }} - - Admission Date: {{ patient.admission_date }} - - Severity Assessment: - {% if patient.severity == "high" %} - URGENT: Immediate attention required - Priority: CRITICAL - {% elif patient.severity == "medium" %} - NOTICE: Review within 24 hours - Priority: MODERATE - {% else %} - ROUTINE: Standard processing - Priority: LOW - {% endif %} - - Case Summary: - {{ patient.patient_name }} ({{ patient.age }}y) admitted with {{ patient.condition }}. - {% if patient.age > 60 %} - Note: Elderly patient - additional monitoring recommended. - {% endif %} - - from: TriageDataGenerator - to: TriageReport - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: - type: template - config: - template: | - TRIAGE QUEUE STATUS - - {% set patients = input | fromjson %} - - Current Queue ({{ patients | length }} patients): - - {% for patient in patients %} - {{ loop.index }}. {{ patient.name }} - Priority: {{ patient.priority | upper }} - Wait Time: {{ patient.wait_time }} min - {% if patient.priority == 'high' %} - → ALERT: Fast-track this patient - {% elif patient.wait_time > 30 %} - → WARNING: Extended wait time - {% endif %} - {% endfor %} - - from: StatusGenerator - to: StatusReport - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: - type: template - config: - template: | - System Status: {{ input }} - Emergency Level: {{ emergency_level | default('None') }} - Timestamp: {{ timestamp | default('Not specified') }} - - - from: DischargeDataGenerator - to: DischargeSummaryFormatter - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - - from: HospitalMetrics - to: MetricsDashboard - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - # Flow to final aggregator - - from: AdmissionReport - to: DailySummary - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - from: TriageReport - to: DailySummary - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - from: StatusReport - to: DailySummary - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - - from: DischargeSummaryFormatter - to: DailySummary - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - - - from: MetricsDashboard - to: DailySummary - trigger: true - condition: 'true' - carry_data: true - keep_message: false - clear_context: false - clear_kept_context: false - processor: null - memory: [] - initial_instruction: '' - start: - - PatientDataGenerator - - TriageDataGenerator - - StatusGenerator - - DischargeDataGenerator - - HospitalMetrics - end: - - DailySummary -version: 0.0.0 -vars: - MODEL_NAME: qwen/qwen3-8b \ No newline at end of file From 6ee14f52e0a0ad1cdcf159cd1707f0f1660ef6dd Mon Sep 17 00:00:00 2001 From: Do Le Long An <85084360+LaansDole@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:50:33 +0700 Subject: [PATCH 23/42] Delete openspec/changes/add-template-formatter-node directory --- .../add-template-formatter-node/tasks.md | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 openspec/changes/add-template-formatter-node/tasks.md diff --git a/openspec/changes/add-template-formatter-node/tasks.md b/openspec/changes/add-template-formatter-node/tasks.md deleted file mode 100644 index 6ac636ee8..000000000 --- a/openspec/changes/add-template-formatter-node/tasks.md +++ /dev/null @@ -1,32 +0,0 @@ -## 1. Implementation - -- [x] 1.1 Create template node configuration schema (`entity/configs/node/template.py`) -- [x] 1.2 Implement template node executor (`runtime/node/executor/template_executor.py`) -- [x] 1.3 Register template node in builtin nodes registry -- [x] 1.4 Add template node field specifications for frontend schema -- [x] 1.5 Write comprehensive unit tests for template node - -## 2. Validation - -- [x] 2.1 Test template rendering with JSON parsing (`fromjson` filter) -- [x] 2.2 Test template rendering with string operations (replace, trim, etc.) -- [x] 2.3 Test template rendering with conditional logic (if/else) -- [x] 2.4 Test template rendering with loops (for loops) -- [x] 2.5 Test error handling for invalid templates -- [x] 2.6 Test error handling for undefined variables -- [x] 2.7 Test log output at different levels (DEBUG, INFO) -- [x] 2.8 Validate YAML workflow configuration with check module - -## 3. Documentation - -- [x] 3.1 Add template node example to yaml_instance directory -- [ ] 3.2 Update workflow authoring guide with template node usage -- [x] 3.3 Document available Jinja2 filters and template syntax (TEMPLATE_NODE_GUIDE.md) -- [x] 3.4 Add comparison with literal node and edge processors (TEMPLATE_NODE_GUIDE.md) - -## 4. Integration - -- [ ] 4.1 Test template node in hospital simulation workflow -- [ ] 4.2 Verify template node output in workflow logs -- [ ] 4.3 Ensure template node works with dynamic execution (map mode) -- [ ] 4.4 Validate template node behavior with empty inputs From b251478c2b3c0f6376e7058d3c56aaed77f5a5a3 Mon Sep 17 00:00:00 2001 From: Do Le Long An <85084360+LaansDole@users.noreply.github.com> Date: Tue, 10 Feb 2026 21:53:33 +0700 Subject: [PATCH 24/42] Delete openspec/changes/add-template-formatter-node directory --- .../add-template-formatter-node/tasks.md | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 openspec/changes/add-template-formatter-node/tasks.md diff --git a/openspec/changes/add-template-formatter-node/tasks.md b/openspec/changes/add-template-formatter-node/tasks.md deleted file mode 100644 index 6ac636ee8..000000000 --- a/openspec/changes/add-template-formatter-node/tasks.md +++ /dev/null @@ -1,32 +0,0 @@ -## 1. Implementation - -- [x] 1.1 Create template node configuration schema (`entity/configs/node/template.py`) -- [x] 1.2 Implement template node executor (`runtime/node/executor/template_executor.py`) -- [x] 1.3 Register template node in builtin nodes registry -- [x] 1.4 Add template node field specifications for frontend schema -- [x] 1.5 Write comprehensive unit tests for template node - -## 2. Validation - -- [x] 2.1 Test template rendering with JSON parsing (`fromjson` filter) -- [x] 2.2 Test template rendering with string operations (replace, trim, etc.) -- [x] 2.3 Test template rendering with conditional logic (if/else) -- [x] 2.4 Test template rendering with loops (for loops) -- [x] 2.5 Test error handling for invalid templates -- [x] 2.6 Test error handling for undefined variables -- [x] 2.7 Test log output at different levels (DEBUG, INFO) -- [x] 2.8 Validate YAML workflow configuration with check module - -## 3. Documentation - -- [x] 3.1 Add template node example to yaml_instance directory -- [ ] 3.2 Update workflow authoring guide with template node usage -- [x] 3.3 Document available Jinja2 filters and template syntax (TEMPLATE_NODE_GUIDE.md) -- [x] 3.4 Add comparison with literal node and edge processors (TEMPLATE_NODE_GUIDE.md) - -## 4. Integration - -- [ ] 4.1 Test template node in hospital simulation workflow -- [ ] 4.2 Verify template node output in workflow logs -- [ ] 4.3 Ensure template node works with dynamic execution (map mode) -- [ ] 4.4 Validate template node behavior with empty inputs From 40cbdbd623fdd2ffe4223c3f52ea9ac3e7927194 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:53:59 +0700 Subject: [PATCH 25/42] feat: rich tooltip component --- frontend/src/components/RichTooltip.vue | 351 ++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 frontend/src/components/RichTooltip.vue diff --git a/frontend/src/components/RichTooltip.vue b/frontend/src/components/RichTooltip.vue new file mode 100644 index 000000000..ead65efc5 --- /dev/null +++ b/frontend/src/components/RichTooltip.vue @@ -0,0 +1,351 @@ + + + + + From bb7e8d2c636604354350576640853cb2e4849c64 Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:56:56 +0700 Subject: [PATCH 26/42] feat: add help content to tooltip --- frontend/src/utils/helpContent.js | 229 ++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 frontend/src/utils/helpContent.js diff --git a/frontend/src/utils/helpContent.js b/frontend/src/utils/helpContent.js new file mode 100644 index 000000000..85e1b4035 --- /dev/null +++ b/frontend/src/utils/helpContent.js @@ -0,0 +1,229 @@ +export const helpContent = { + // Start Node Help + startNode: { + title: "Start Node", + description: "The entry point for your workflow. All nodes connected to the Start node will run in parallel when the workflow launches.", + examples: [ + "Connect multiple nodes to start them simultaneously", + "The first nodes to execute receive your initial input" + ], + learnMoreUrl: "/tutorial#2-create-nodes" + }, + + // Workflow Node Types + workflowNode: { + agent: { + title: "Agent Node", + description: "An AI agent that can reason, generate content, and use tools. Agents receive messages and produce responses based on their configuration.", + examples: [ + "Content generation (writing, coding, analysis)", + "Decision making and routing", + "Tool usage (search, file operations, API calls)" + ], + learnMoreUrl: "/tutorial#agent-node" + }, + human: { + title: "Human Node", + description: "Pauses workflow execution and waits for human input. Use this to review content, make decisions, or provide feedback.", + examples: [ + "Review and approve generated content", + "Provide additional instructions or corrections", + "Choose between workflow paths" + ], + learnMoreUrl: "/tutorial#human-node" + }, + python: { + title: "Python Node", + description: "Executes Python code in a sandboxed environment. The code runs in the workspace directory and can access uploaded files.", + examples: [ + "Data processing and analysis", + "Running generated code", + "File manipulation" + ], + learnMoreUrl: "/tutorial#python-node" + }, + passthrough: { + title: "Passthrough Node", + description: "Passes messages to the next node without modification. Useful for workflow organization and filtering outputs in loops.", + examples: [ + "Preserve initial context in loops", + "Filter redundant outputs", + "Organize workflow structure" + ], + learnMoreUrl: "/tutorial#passthrough-node" + }, + literal: { + title: "Literal Node", + description: "Outputs fixed text, ignoring all input. Use this to inject instructions or context at specific points in the workflow.", + examples: [ + "Add fixed instructions before a node", + "Inject context or constraints", + "Provide test data" + ], + learnMoreUrl: "/tutorial#literal-node" + }, + loop_counter: { + title: "Loop Counter Node", + description: "Limits loop iterations. Only produces output when the maximum count is reached, helping control infinite loops.", + examples: [ + "Prevent runaway loops", + "Set maximum revision cycles", + "Control iterative processes" + ], + learnMoreUrl: "/tutorial#loop-counter-node" + }, + subgraph: { + title: "Subgraph Node", + description: "Embeds another workflow as a reusable module. Enables modular design and workflow composition.", + examples: [ + "Reuse common patterns across workflows", + "Break complex workflows into manageable pieces", + "Share workflows between teams" + ], + learnMoreUrl: "/tutorial#subgraph-node" + }, + unknown: { + title: "Workflow Node", + description: "A node in your workflow. Click to view and edit its configuration.", + learnMoreUrl: "/tutorial#2-create-nodes" + } + }, + + // Workflow Edge Help + edge: { + basic: { + title: "Connection", + description: "Connects two nodes to control information flow and execution order. The upstream node's output becomes the downstream node's input.", + examples: [ + "Data flows from source to target", + "Target executes after source completes" + ], + learnMoreUrl: "/tutorial#what-is-an-edge" + }, + trigger: { + enabled: { + description: "This connection triggers the downstream node to execute.", + }, + disabled: { + description: "This connection passes data but does NOT trigger execution. The downstream node only runs if triggered by another edge.", + } + }, + condition: { + hasCondition: { + description: "This connection has a condition. It only activates when the condition evaluates to true.", + learnMoreUrl: "/tutorial#edge-condition" + } + } + }, + + // Context Menu Actions + contextMenu: { + createNode: { + description: "Create a new node in your workflow. Choose from Agent, Human, Python, and other node types.", + }, + copyNode: { + description: "Duplicate this node with all its settings. The copy will have a blank ID that you must fill in.", + }, + deleteNode: { + description: "Remove this node and all its connections from the workflow.", + }, + deleteEdge: { + description: "Remove this connection between nodes.", + }, + createNodeButton: { + description: "Open the node creation form. You can also right-click the canvas to create a node at a specific position.", + }, + configureGraph: { + description: "Configure workflow-level settings like name, description, and global variables.", + }, + launch: { + description: "Run your workflow with a task prompt. The workflow will execute and show you the results.", + }, + createEdge: { + description: "Create a connection between nodes. You can also drag from a node's handle to create connections visually.", + }, + manageVariables: { + description: "Define global variables (like API keys) that all nodes can access using ${VARIABLE_NAME} syntax.", + }, + manageMemories: { + description: "Configure memory modules for long-term information storage and retrieval across workflow runs.", + }, + renameWorkflow: { + description: "Change the name of this workflow file.", + }, + copyWorkflow: { + description: "Create a duplicate of this entire workflow with a new name.", + } + } +} + +/** + * Get help content by key path + * @param {string} key - Dot-separated path to content (e.g., 'workflowNode.agent') + * @returns {Object} Help content object or fallback + */ +export function getHelpContent(key) { + const keys = key.split('.') + let content = helpContent + + for (const k of keys) { + if (content && typeof content === 'object' && k in content) { + content = content[k] + } else { + console.warn(`[HelpContent] Missing content for key: ${key}`) + return { + description: "Help content coming soon. Check the tutorial for more information.", + learnMoreUrl: "/tutorial" + } + } + } + + // Ensure we return an object with at least a description + if (typeof content === 'string') { + return { description: content } + } + + return content || { description: "Help content coming soon." } +} + +/** + * Get node-specific help content based on node type + * @param {string} nodeType - The type of node (agent, human, python, etc.) + * @returns {Object} Help content for that node type + */ +export function getNodeHelp(nodeType) { + const type = (nodeType || 'unknown').toLowerCase() + return getHelpContent(`workflowNode.${type}`) +} + +/** + * Get edge help content based on edge properties + * @param {Object} edgeData - The edge data object + * @returns {Object} Combined help content for the edge + */ +export function getEdgeHelp(edgeData) { + const base = { ...helpContent.edge.basic } + + // Add trigger information + const trigger = edgeData?.trigger !== undefined ? edgeData.trigger : true + if (!trigger) { + base.description += " " + helpContent.edge.trigger.disabled.description + } + + // Add condition information + if (edgeData?.condition) { + base.description += " " + helpContent.edge.condition.hasCondition.description + if (!base.learnMoreUrl) { + base.learnMoreUrl = helpContent.edge.condition.hasCondition.learnMoreUrl + } + } + + return base +} + +export default { + helpContent, + getHelpContent, + getNodeHelp, + getEdgeHelp +} From bdcc61f668bb009319131f4a87b586a1a322363f Mon Sep 17 00:00:00 2001 From: laansdole Date: Sun, 8 Feb 2026 10:59:22 +0700 Subject: [PATCH 27/42] feat: wrapping nodes with tooltip content --- frontend/src/components/StartNode.vue | 14 ++-- frontend/src/components/WorkflowEdge.vue | 30 +++++++ frontend/src/components/WorkflowNode.vue | 68 ++++++++------- frontend/src/pages/WorkflowView.vue | 102 ++++++++++++++--------- 4 files changed, 140 insertions(+), 74 deletions(-) diff --git a/frontend/src/components/StartNode.vue b/frontend/src/components/StartNode.vue index 36308697c..1aa0b20ab 100755 --- a/frontend/src/components/StartNode.vue +++ b/frontend/src/components/StartNode.vue @@ -1,6 +1,8 @@ diff --git a/frontend/src/components/WorkflowNode.vue b/frontend/src/components/WorkflowNode.vue index cfdc8dd92..d6c205840 100755 --- a/frontend/src/components/WorkflowNode.vue +++ b/frontend/src/components/WorkflowNode.vue @@ -3,6 +3,8 @@ import { computed, ref, onMounted, onUnmounted, watch } from 'vue' import { Handle, Position } from '@vue-flow/core' import { getNodeStyles } from '../utils/colorUtils.js' import { spriteFetcher } from '../utils/spriteFetcher.js' +import RichTooltip from './RichTooltip.vue' +import { getNodeHelp } from '../utils/helpContent.js' const props = defineProps({ id: { @@ -37,6 +39,8 @@ const nodeDescription = computed(() => props.data?.description || '') const isActive = computed(() => props.isActive) const dynamicStyles = computed(() => getNodeStyles(nodeType.value)) +const nodeHelpContent = computed(() => getNodeHelp(nodeType.value)) + // Compute the current sprite path based on active state and walking frame const currentSprite = computed(() => { if (!props.sprite) return '' @@ -83,40 +87,42 @@ onUnmounted(() => {