From 9393bf76dcb57ebd5d48a5456d2a3872d76d30e5 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 13:12:28 +0930 Subject: [PATCH 01/26] feat(spec): add core A2UI templates specification, version 0.1 schema, and examples --- specification/proposals/templates/README.md | 582 ++++++++++++++++++ .../templates/examples/basic_cards.yaml | 69 +++ .../templates/examples/feedback_item.yaml | 46 ++ .../templates/examples/goal_item.yaml | 59 ++ .../templates/examples/nested_lists.yaml | 86 +++ .../templates/examples/salary_card.yaml | 116 ++++ .../templates/examples/section_card.yaml | 52 ++ .../templates/examples/slot_composition.yaml | 80 +++ .../templates/examples/team_card.yaml | 68 ++ .../examples/team_feedback_board.yaml | 64 ++ .../templates/examples/team_goal_list.yaml | 65 ++ .../examples/team_member_knowledge_panel.yaml | 78 +++ .../templates/examples/team_roster.yaml | 29 + .../templates/examples/two_column_layout.yaml | 40 ++ .../templates/examples/user_profile.yaml | 38 ++ .../templates/schema/template_definition.json | 273 ++++++++ 16 files changed, 1745 insertions(+) create mode 100644 specification/proposals/templates/README.md create mode 100644 specification/proposals/templates/examples/basic_cards.yaml create mode 100644 specification/proposals/templates/examples/feedback_item.yaml create mode 100644 specification/proposals/templates/examples/goal_item.yaml create mode 100644 specification/proposals/templates/examples/nested_lists.yaml create mode 100644 specification/proposals/templates/examples/salary_card.yaml create mode 100644 specification/proposals/templates/examples/section_card.yaml create mode 100644 specification/proposals/templates/examples/slot_composition.yaml create mode 100644 specification/proposals/templates/examples/team_card.yaml create mode 100644 specification/proposals/templates/examples/team_feedback_board.yaml create mode 100644 specification/proposals/templates/examples/team_goal_list.yaml create mode 100644 specification/proposals/templates/examples/team_member_knowledge_panel.yaml create mode 100644 specification/proposals/templates/examples/team_roster.yaml create mode 100644 specification/proposals/templates/examples/two_column_layout.yaml create mode 100644 specification/proposals/templates/examples/user_profile.yaml create mode 100644 specification/proposals/templates/schema/template_definition.json diff --git a/specification/proposals/templates/README.md b/specification/proposals/templates/README.md new file mode 100644 index 0000000000..a53b5c487b --- /dev/null +++ b/specification/proposals/templates/README.md @@ -0,0 +1,582 @@ +# A2UI Templates Specification + +## Abstract + +This document defines the authoritative specification for the **A2UI Templates System**. + +Templates provide a declarative, human-readable mechanism for authoring reusable UI subtrees in nested YAML or via native programmatic render functions. At generation time, an agent or Large Language Model (LLM) outputs compact, high-level template invocations (such as `UserProfile("u1", "Alice")` or `PayrollSummary("Eng", true)`). The backend SDK expands these invocations into standard, flat A2UI Basic Catalog components in a single synchronous pass before emitting standard A2UI protocol messages (`updateComponents`) to the client. + +This architecture achieves three fundamental objectives: +1. **Human Readability**: Layouts are authored as clean, natural tree hierarchies in YAML or native language code, eliminating artificial IDs and flat array boilerplate. +2. **Token Efficiency & Context Optimization**: LLMs emit concise function-like signatures rather than verbose, deeply nested UI component trees, saving prompt context and generation tokens. +3. **Zero Client Overhead**: Client renderers (@a2ui/react, @a2ui/lit, @a2ui/angular, @a2ui/flutter) implement only standard Basic Catalog primitives (`Card`, `Column`, `Row`, `Text`, `Divider`, `Icon`, `Button`). No custom client widgets, dynamic code deployment, or renderer plugins are required. + +--- + +## 1. Architectural Overview & Execution Pipeline + +The template engine operates entirely server-side within the A2UI Agent SDK: + +``` ++-------------------------------------------------------------------------------+ +| 1. Authoring & Registration | +| - Static YAML files (with multi-doc '---' support & cross-references) | +| - Dynamic resolvers (data-binding mode & programmatic AST mode) | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| 2. Synthetic Catalog Generation | +| - TemplateProcessor scans registered templates | +| - Synthesizes virtual A2UI Component Catalog JSON schema | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| 3. Model Prompt Generation & Inference | +| - PromptGenerator injects template signatures into system prompt | +| - LLM emits compact Express DSL / JSON: | +| root = TeamRoster("Engineering", [TeamCard("Core", [...])]) | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| 4. Synchronous Template Expansion (TemplateProcessor) | +| - Resolves dynamic callbacks or executes programmatic render functions | +| - Substitutes typed parameters & evaluates dot-notation paths | +| - Unrolls server loops & plumbs component slot arguments | +| - Assigns deterministic synthetic IDs: {parent}_{slot}_{index}_{type} | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| 5. Standard A2UI Protocol Emission | +| - Emits standard createSurface and updateComponents messages | +| - Payload contains 100% standard Basic Catalog primitives | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| 6. Client Renderer Execution | +| - Standard Basic Catalog renderers paint the flat component graph | +| - Binds reactive two-way paths to client DataModel | ++-------------------------------------------------------------------------------+ +``` + +--- + +## 2. Template Taxonomy + +A2UI defines three template modalities: + +### A. Declarative Static Templates (YAML) +Declarative static templates define immutable layout trees authored in YAML files. All parameter placeholders are populated directly from arguments provided by the model or caller. + +```yaml +version: "0.1" +templateId: UserProfile +description: Standard identity card for team members. +parameters: + userId: + type: string + description: Unique user identifier. + userName: + type: string + description: Full name of the user. + role: + type: string + description: Position or title within the team. + default: Team Member + +layout: + component: Card + child: + component: Column + children: + - component: Icon + name: person + - component: Text + text: ${userName} + variant: h3 + - component: Text + text: ${role} + variant: caption +``` + +### B. Dynamic Templates: Data-Binding Mode (`resolver + layout`) +Data-binding dynamic templates decouple public model parameters from private or live backend data. +- **Public Surface**: The model only sees and outputs high-level lookup parameters (e.g. `employeeId: "emp_101"`). +- **Server Resolver**: At expansion time, a backend callback executes (e.g., querying an internal HR database or API) and produces a data dictionary. +- **Layout Inflation**: The resolved dictionary is merged with the caller's arguments and applied to an underlying static YAML layout. + +```python +# Server Registration +dynamic_salary = DynamicTemplate( + template_id="EmployeeSalaryCard", + resolver=fetch_private_compensation, + layout=salary_yaml_layout, + description="Verified compensation card. Pass only employeeId.", +) +``` + +**Security Rationale**: Confidential numbers (e.g., executive salaries, PII) never enter the model's prompt context window or inference transcript. + +### C. Dynamic Templates: Programmatic Render Mode (`render_fn`) +Programmatic dynamic templates bypass static YAML layouts entirely. Instead, a developer writes a native function in the host language (Python, Dart, etc.) that accepts parameters and directly returns a UI component tree. + +```python +def render_payroll_summary(department: str = "Engineering", includeBonus: bool = True) -> dict: + total_base = sum(e.salary for e in db.get_dept(department)) + total_bonus = sum(e.bonus for e in db.get_dept(department)) + + rows = [] + for emp in db.get_dept(department): + cols = [ + {"component": "Text", "text": emp.name, "variant": "body"}, + {"component": "Text", "text": f"${emp.salary:,}", "variant": "body"} + ] + if includeBonus: + cols.append({"component": "Text", "text": f"${emp.bonus:,}", "variant": "body"}) + rows.append({"component": "Row", "justify": "spaceBetween", "children": cols}) + + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + {"component": "Text", "text": f"Payroll Summary: {department}", "variant": "h2"}, + {"component": "Divider", "axis": "horizontal"}, + *rows, + {"component": "Divider", "axis": "horizontal"}, + {"component": "Text", "text": f"Total Budget: ${total_base + (total_bonus if includeBonus else 0):,}", "variant": "h3"} + ] + } + } + +payroll_tmpl = DynamicTemplate( + template_id="PayrollSummary", + render=render_payroll_summary, + description="Dynamic payroll calculation matrix.", +) +``` + +**Advantages**: +- Full host language power: arbitrary `for` loops, mathematical calculations, currency formatting, conditionals, and recursion. +- Polymorphic return values: accepts raw dictionaries/maps, dataclasses, or typesafe fluent builder objects (`Card()`, `Column()`). + +--- + +## 3. Loop Processing: Server Unrolling vs. Client Data Model + +A critical question in template design is: **When does the template engine unroll loops vs. when are collections handled by the client runtime?** + +### Comparison Matrix + +| Dimension | Server-Side Template Unrolling | Client-Side DataModel Loop | +| :--- | :--- | :--- | +| **Execution Point** | Backend SDK (`TemplateProcessor`) during template expansion. | Client Renderer runtime during paint/state updates. | +| **Data Source** | Static YAML literals, LLM invocation arguments, or server resolver output. | Client `DataModel` tree (e.g. `/session/cart/items`). | +| **Component Output** | Emits $N$ concrete Basic Catalog component nodes with synthesized unique IDs. | Emits a single repeater/collection component with a `DataBinding` path. | +| **Client Requirement** | Zero. Standard Basic Catalog renderers render it like any other static UI. | Requires client-side repeater support or SDK session re-renders. | +| **Dynamic Mutation** | Immutable once generated unless the agent emits a new `updateComponents` message. | Reactively re-renders when the client data model updates (e.g. via button events). | + +### Example 1: Server-Side Template Loop Unrolling +A template author specifies a loop over a parameter array: + +#### Template Definition: +```yaml +version: "0.1" +templateId: TeamGoalList +parameters: + teamName: {type: string} + goals: {type: array} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: "Objectives: ${teamName}" + variant: h2 + - component: Column + children: + loop: + param: goals + as: goal + item: + component: Row + justify: spaceBetween + children: + - component: Text + text: ${goal.title} + - component: Text + text: ${goal.priority} + variant: caption +``` + +#### Model Invocation: +```text +root = TeamGoalList("Core Team", [ + {title: "Ship Protocol", priority: "High"}, + {title: "Write Tests", priority: "Medium"} +]) +``` + +#### Expanded Output Sent to Client: +The engine unrolls the loop into concrete, synthetic child components: +```json +[ + {"id": "root", "component": "Card", "child": "root_child_column"}, + {"id": "root_child_column", "component": "Column", "children": ["root_child_column_children_0_text", "root_child_column_children_1_column"]}, + {"id": "root_child_column_children_0_text", "component": "Text", "text": "Objectives: Core Team", "variant": "h2"}, + {"id": "root_child_column_children_1_column", "component": "Column", "children": [ + "root_child_column_children_1_column_goals_0_row", + "root_child_column_children_1_column_goals_1_row" + ]}, + {"id": "root_child_column_children_1_column_goals_0_row", "component": "Row", "justify": "spaceBetween", "children": [ + "root_child_column_children_1_column_goals_0_row_children_0_text", + "root_child_column_children_1_column_goals_0_row_children_1_text" + ]}, + {"id": "root_child_column_children_1_column_goals_0_row_children_0_text", "component": "Text", "text": "Ship Protocol"}, + {"id": "root_child_column_children_1_column_goals_0_row_children_1_text", "component": "Text", "text": "High", "variant": "caption"}, + {"id": "root_child_column_children_1_column_goals_1_row", "component": "Row", "justify": "spaceBetween", "children": [ + "root_child_column_children_1_column_goals_1_row_children_0_text", + "root_child_column_children_1_column_goals_1_row_children_1_text" + ]}, + {"id": "root_child_column_children_1_column_goals_1_row_children_0_text", "component": "Text", "text": "Write Tests"}, + {"id": "root_child_column_children_1_column_goals_1_row_children_1_text", "component": "Text", "text": "Medium", "variant": "caption"} +] +``` + +--- + +## 4. Relationship to the A2UI Data Model + +Templates and the A2UI Data Model operate in complementary scopes: +- **Templates** expand during **server-side inference / response formulation**. +- **Data Model** operates during **client-side runtime interaction & reactivity**. + +Templates interact with the client Data Model in three distinct patterns: + +### Pattern 1: Dynamic Path Plumbing (Path Interpolation) +Templates can accept path prefixes or IDs as parameters, constructing reactive client-side binding paths: + +```yaml +version: "0.1" +templateId: BoundLiveMetric +parameters: + metricKey: {type: string} + label: {type: string} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${label} + - component: Text + text: + path: "/system/metrics/${metricKey}/currentValue" +``` + +**Expanded Output:** +```json +{ + "id": "root_val", + "component": "Text", + "text": { + "path": "/system/metrics/cpuLoad/currentValue" + } +} +``` +*Client Behavior*: The client renderer attaches a live subscriber to `/system/metrics/cpuLoad/currentValue`. When telemetry updates arrive via `updateDataModel`, the component automatically re-renders without server roundtrips. + +### Pattern 2: Direct `DataBinding` Passthrough +When a template parameter has `type: object`, an agent or caller can pass an explicit A2UI binding object: + +#### Model Invocation: +```text +root = MetricCard("Memory Used", {path: "/device/mem_percent"}) +``` + +#### Template Layout: +```yaml +version: "0.1" +templateId: MetricCard +parameters: + title: {type: string} + val: {type: object} +layout: + component: Column + children: + - component: Text + text: ${title} + - component: Text + text: ${val} +``` + +**Expanded Output:** +Because `${val}` is an exact-match substitution, the dictionary type is strictly preserved: +```json +{ + "component": "Text", + "text": {"path": "/device/mem_percent"} +} +``` + +### Pattern 3: Hardcoded Session State References +Templates can embed constant client bindings for global session preferences: + +```yaml +layout: + component: Text + text: + path: "/session/currentUser/displayName" +``` + +--- + +## 5. The Synthetic Catalog System + +To make templates discoverable and invoke-able by LLMs, the backend converts registered templates into a **Synthetic Component Catalog**. + +### The Catalog Synthesis Algorithm +When `TemplateProcessor(templates)` is initialized: +1. It copies the base catalog (e.g. Basic Catalog v0.9.1). +2. For each registered `StaticTemplate` and `DynamicTemplate`: + - It registers a new component definition under `components[templateId]`. + - It maps semantic parameter definitions to JSON schema properties: + - `string` $\rightarrow$ `{"type": "string"}` + - `number` / `integer` $\rightarrow$ `{"type": "number"}` / `{"type": "integer"}` + - `boolean` $\rightarrow$ `{"type": "boolean"}` + - `enum` $\rightarrow$ `{"type": "string", "enum": param.values}` + - `object` $\rightarrow$ `{"type": "object", "properties": ...}` + - `array` $\rightarrow$ `{"type": "array", "items": ...}` + - `child` $\rightarrow$ `{"$ref": "#/$defs/ComponentReference"}` + - `children` $\rightarrow$ `{"$ref": "#/$defs/ComponentReferenceList"}` + - It copies the `description` to provide context for the model. +3. The synthetic catalog is passed to `PromptGenerator` (Express, Elemental, Atom, Direct JSON), which automatically writes the syntax rules and documentation into the system prompt. + +### Surface Isolation Boundary +``` +[LLM Agent] <---> [Synthetic Catalog (Templates + Primitives)] <---> [TemplateProcessor] + | + (Expands to Primitives) + | + v +[Client Renderer] <---------------- (Standard Basic Catalog Only) <----------+ +``` +The synthetic catalog is an ephemeral compile-time construct. The client renderer is completely unaware that templates exist. + +--- + +## 6. Parameter Expressions & Substitution Engine + +The template substitution engine replaces expressions according to strict typing and evaluation rules. + +### Rule 1: Exact Match Substitution (Type Preserving) +If a string field value exactly matches the parameter token regex `^\$\{([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}$`: +- The entire field is replaced by the native runtime value of the parameter. +- **Integers remain integers**: `level: ${userLevel}` $\rightarrow$ `level: 4` (not `"4"`). +- **Booleans remain booleans**: `checked: ${isActive}` $\rightarrow$ `checked: true` (not `"true"`). +- **Objects and Arrays remain native structures**: `items: ${memberList}` $\rightarrow$ `items: [{...}, {...}]`. + +### Rule 2: Embedded String Interpolation +If a parameter token appears alongside other characters (e.g. `"Hello, ${userName}!"` or `"${dept} - ${code}"`): +- All tokens are replaced by their string representations. +- Evaluates to a native string. + +### Rule 3: Deep Dot-Notation for Structured Objects +Templates natively support deep dot-notation paths on object parameters: + +#### Invocation: +```text +root = AccountCard({ + account: { + id: "acc_99", + profile: {name: "Alpha Corp", tier: "Enterprise"} + } +}) +``` + +#### Template Layout: +```yaml +version: "0.1" +templateId: AccountCard +parameters: + account: {type: object} +layout: + component: Card + child: + component: Text + text: "${account.profile.name} (${account.profile.tier})" +``` + +**Expanded Output:** +```json +{ + "component": "Text", + "text": "Alpha Corp (Enterprise)" +} +``` + +### Rule 4: String Format AST Expressions +For complex string formatting across multiple languages: +```yaml +format: "Level {lvl} ({pts} points)" +args: + lvl: ${level} + pts: ${experiencePoints} +``` + +### Rule 5: Token Escaping +To emit a literal `${foo}` string without triggering parameter substitution, escape the leading dollar sign with a backslash: +- `\${keep_literal}` $\rightarrow$ evaluates to literal `"${keep_literal}"`. + +### Rule 6: Missing Values & Defaults +- If an argument is omitted but has a declared `default` in its parameter schema, the `default` is used. +- If an argument is omitted, has no `default`, and is not required: the property is omitted from the synthesized component (it is not emitted as `null`). +- If an argument is required and missing: expansion raises a `TemplateParameterError`. + +--- + +## 7. Deterministic Synthetic ID Generation Algorithm + +To guarantee collision-free component IDs across multiple template instances and deep nesting, implementations in all languages (Python, Dart, TypeScript) must implement the following deterministic naming rules: + +### ID Synthesis Specification + +``` +Let instance_id be the ID passed to expand_template(instance_id, template_id, params) + +1. Root Node: + id = instance_id + +2. Single Child Slot (e.g. card.child): + id = "{parent_id}_{slot_name}_{normalized_type}" + (where normalized_type is the component or template type converted to lowercase) + Example: "root_child_column" + +3. Multi-Child List Slot (e.g. column.children[i]): + id = "{parent_id}_{slot_name}_{i}_{normalized_type}" + (where normalized_type is the component or template type converted to lowercase) + Example: "root_children_0_text", "root_children_1_button" + +4. Loop Iteration Item (e.g. loop over param "members", iteration index i): + id = "{parent_id}_{param_name}_{i}_{normalized_type}" + (where normalized_type is the component or template type converted to lowercase) + Example: "team_col_members_0_userprofile" + +5. Authored Explicit IDs: + If a component in the template specifies an explicit id (e.g. id: "hero_img"): + id = "{instance_id}_{authored_id}" + Example: "my_card_hero_img" +``` + +This guarantees that two instances of the same template (`card_1` and `card_2`) on the same surface will never produce colliding component IDs. + +--- + +## 8. Higher-Order Container Templates (`child` and `children` Slots) + +Templates can define structural containers that receive caller-provided components. + +### Example: `SectionCard` Container +```yaml +version: "0.1" +templateId: SectionCard +parameters: + title: {type: string} + headerAction: {type: child} + children: {type: children} + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${title} + variant: h2 + - component: Column + children: ${headerAction} + - component: Divider + axis: horizontal + - component: Column + children: ${children} +``` + +#### Model Invocation: +```text +root = SectionCard( + "Project Status", + Button("Refresh", {action: "reload"}), + [ + Text("Phase 1: Complete"), + Text("Phase 2: In Progress") + ] +) +``` + +The template engine replaces `${headerAction}` with the synthesized single child node, and concatenates the `${children}` component list into the body column. + +--- + +## 9. Error Handling, Cycle Guards & Safety + +### Recursion & Circular Reference Detection +Templates can invoke other templates. However, circular references (`A` invokes `B`, which invokes `A`) must be caught immediately to prevent stack overflow. + +- **Call Stack Tracking**: The `TemplateProcessor` maintains an active `_call_stack: Set[str]` throughout expansion. +- **Cycle Guard**: Before expanding any template, if `template_id in _call_stack`, expansion terminates immediately with `TemplateCycleError: Circular template reference detected: A -> B -> A`. +- **Maximum Depth Guard**: An absolute limit (`MAX_EXPANSION_DEPTH = 32`) enforces termination even in non-identical runaway recursion. + +### Standard Error Hierarchy +1. `TemplateNotFoundError`: Attempted to expand a `templateId` not present in the catalog. +2. `TemplateParameterError`: Missing required parameter or invalid parameter type. +3. `TemplateCycleError`: Circular reference detected during expansion. +4. `TemplateDepthExceededError`: Expansion exceeded maximum recursion depth. +5. `TemplateResolverError`: Exception raised by a dynamic template resolver function. + +--- + +## 10. Multi-Document YAML Streams (`---`) + +To streamline template maintenance, multiple templates can be defined in a single `.yaml` file separated by `---`: + +- **Order Independent**: Templates can reference templates declared later in the file (forward references). +- **Batch Registration**: Loading a multi-doc file registers all defined templates atomically into the `TemplateProcessor`. + +--- + +## 11. Template Versioning & Schema Evolution + +To ensure robust backward compatibility as template syntax and AST features evolve across future protocol releases, every template definition must declare a top-level `version` field. + +### Versioning Rules: +1. **Mandatory Version Field**: Every YAML document (and each document within multi-doc streams separated by `---`) must include `version: "0.1"`. +2. **Strict Schema Validation**: The canonical JSON schema enforces `"version": {"type": "string", "const": "0.1"}`. Attempting to inflate a template with an unsupported version raises an explicit `TemplateVersionError` / `ValueError`. +3. **Future Extensibility**: As new template language capabilities are standardized (e.g. conditional slot rendering, client-expanded template directives), new version identifiers (such as `"0.2"` or `"1.0"`) allow engines to select the appropriate parser/evaluator without breaking legacy templates. + +--- + +## 12. Conformance Checklist for SDK Implementations + +An implementation of the A2UI Template Engine in any language (Python, Dart, TypeScript, Go, etc.) is conformant if and only if it satisfies the following test requirements: + +- [ ] **Template Versioning**: Validates that all ingested templates declare `version: "0.1"` and rejects unsupported version strings. +- [ ] **Multi-Document Ingestion**: Parses single-doc and multi-doc (`---`) YAML streams. +- [ ] **Forward Reference Resolution**: Expands templates regardless of registration order. +- [ ] **Strict Native Type Preservation**: Exact `${param}` substitutions preserve `int`, `float`, `bool`, `dict`, and `list` types. +- [ ] **Dot-Notation Path Traversal**: Resolves arbitrary depth paths (e.g. `${user.details.address.zip}`). +- [ ] **Inline and Named Loop Expansion**: Accurately unrolls arrays into synthesized component subtrees with indexed child IDs. +- [ ] **Deterministic Synthetic IDs**: Emits canonical hierarchical IDs matching the `{parent}_{slot}_{index}_{type}` specification. +- [ ] **Two-Way DataModel Passthrough**: Preserves `{path: "..."}` dictionary bindings without string conversion. +- [ ] **Cycle and Recursion Safeguards**: Catches circular references and depth violations with explicit exceptions. +- [ ] **Synthetic Catalog Generation**: Generates valid A2UI JSON schema catalogs matching input parameter definitions. +- [ ] **Polymorphic Dynamic Templates**: Supports both resolver-based static bindings and programmatic render functions. + diff --git a/specification/proposals/templates/examples/basic_cards.yaml b/specification/proposals/templates/examples/basic_cards.yaml new file mode 100644 index 0000000000..ab6b8bfc8a --- /dev/null +++ b/specification/proposals/templates/examples/basic_cards.yaml @@ -0,0 +1,69 @@ +version: "0.1" +templateId: BasicUserProfile +description: Standard identity card for team members. +parameters: + userId: + type: string + description: Unique user identifier. + userName: + type: string + description: Full name of the user. + role: + type: string + description: Position or title within the team. + default: Team Member + +layout: + component: Card + child: + component: Column + align: center + children: + - component: Icon + name: person + - component: Text + text: ${userName} + variant: h3 + - component: Text + text: ${role} + variant: caption + +sampleData: + userId: usr_101 + userName: Alice Smith + role: Lead Architect + +--- + +version: "0.1" +templateId: BasicMetricCard +description: Metric display card supporting both literal values and dynamic data model bindings. +parameters: + title: + type: string + description: Metric title. + metricValue: + type: object + description: "Literal value or reactive DataBinding object (e.g. {path: '/telemetry/cpu'})." + status: + type: enum + values: ["normal", "warning", "critical"] + default: "normal" + +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${title} + variant: caption + - component: Text + text: ${metricValue} + variant: h1 + +sampleData: + title: CPU Load + metricValue: + path: /system/metrics/cpuLoad + status: normal diff --git a/specification/proposals/templates/examples/feedback_item.yaml b/specification/proposals/templates/examples/feedback_item.yaml new file mode 100644 index 0000000000..bb170c2333 --- /dev/null +++ b/specification/proposals/templates/examples/feedback_item.yaml @@ -0,0 +1,46 @@ +version: "0.1" +templateId: FeedbackItem +description: Card showing feedback note, author, and rating. + +parameters: + author: + type: string + title: Author Name + description: The name of the colleague or customer providing feedback. + note: + type: string + title: Feedback Note + description: The written feedback or retrospective comment. + rating: + type: number + title: Feedback Rating + description: Score from 1 to 5. + minimum: 1 + maximum: 5 + default: 5 + +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${note} + variant: body + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${author} + variant: caption + - component: Text + text: "Rating: ${rating}/5" + variant: caption + +sampleData: + author: Dr. Elena Vance + note: A2UI templates are fast and easy to compose. + rating: 5 diff --git a/specification/proposals/templates/examples/goal_item.yaml b/specification/proposals/templates/examples/goal_item.yaml new file mode 100644 index 0000000000..a9009555b0 --- /dev/null +++ b/specification/proposals/templates/examples/goal_item.yaml @@ -0,0 +1,59 @@ +version: "0.1" +templateId: GoalItem +description: Card showing an individual objective with priority, title, target date, and action button. + +parameters: + title: + type: string + title: Goal Title + description: Summary title of the objective. + priority: + type: enum + title: Priority Level + description: Urgency rating for the objective. + values: + - High + - Medium + - Low + default: Medium + targetDate: + type: string + title: Target Date + description: Target completion date in YYYY-MM-DD format. + default: "2026-12-31" + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "Priority: ${priority}" + variant: caption + - component: Icon + name: star + - component: Text + text: ${title} + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "Due: ${targetDate}" + variant: caption + - component: Button + child: + component: Text + text: View Details + +sampleData: + title: Launch A2UI SDK v1.0 + priority: High + targetDate: "2026-09-30" diff --git a/specification/proposals/templates/examples/nested_lists.yaml b/specification/proposals/templates/examples/nested_lists.yaml new file mode 100644 index 0000000000..8afd3d5bab --- /dev/null +++ b/specification/proposals/templates/examples/nested_lists.yaml @@ -0,0 +1,86 @@ +version: "0.1" +templateId: NestedTeamRoster +description: Team directory roster with styled header banner and nested team lists. +parameters: + directoryTitle: + type: string + description: Title text for the team directory. + teams: + type: array + description: List of team objects or TeamCard children. + default: [] + +layout: + component: Column + children: + - component: Text + text: ${directoryTitle} + variant: h1 + - component: Divider + axis: horizontal + - component: Column + children: + loop: + param: teams + template: NestedTeamCard + +sampleData: + directoryTitle: Global Engineering Directory + teams: + - teamName: Core Architecture + members: + - userId: u1 + userName: Dr. Elena Vance + role: Principal Architect + - userId: u2 + userName: Marcus Vance + role: Streaming Lead + +--- + +version: "0.1" +templateId: NestedTeamCard +description: Team container card displaying member cards. +parameters: + teamName: + type: string + description: Name of the engineering team. + members: + type: array + description: List of team members. + default: [] + +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${teamName} + variant: h2 + - component: Divider + axis: horizontal + - component: Column + children: + loop: + param: members + as: member + item: + component: Row + justify: spaceBetween + children: + - component: Text + text: ${member.userName} + - component: Text + text: ${member.role} + variant: caption + +sampleData: + teamName: Core Architecture + members: + - userId: u1 + userName: Dr. Elena Vance + role: Principal Architect + - userId: u2 + userName: Marcus Vance + role: Streaming Lead diff --git a/specification/proposals/templates/examples/salary_card.yaml b/specification/proposals/templates/examples/salary_card.yaml new file mode 100644 index 0000000000..f4b847e746 --- /dev/null +++ b/specification/proposals/templates/examples/salary_card.yaml @@ -0,0 +1,116 @@ +version: "0.1" +templateId: SalaryCard +description: Layout card for employee compensation package with security verification badge. + +parameters: + employeeName: + type: string + title: Employee Full Name + description: Full name of the verified employee. + role: + type: string + title: Job Title + description: Official company role. + baseSalary: + type: string + title: Base Salary + description: Annual base compensation. + annualBonus: + type: string + title: Annual Bonus + description: Target annual incentive bonus. + equity: + type: string + title: Equity Grants + description: Stock units or RSU package. + clearanceLevel: + type: string + title: Security Clearance + description: Confidentiality level. + default: "Level 4 - Confidential" + verifiedAt: + type: string + title: Verification Date + description: Timestamp of record retrieval. + default: "2026-08-13" + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Row + align: center + children: + - component: Icon + name: lock + - component: Text + text: "Verified Compensation" + variant: caption + - component: Text + text: ${clearanceLevel} + variant: caption + - component: Column + children: + - component: Text + text: ${employeeName} + variant: h3 + id: name_txt + - component: Text + text: ${role} + variant: body + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + children: + - component: Column + children: + - component: Text + text: "Base Salary" + variant: caption + - component: Text + text: ${baseSalary} + variant: h4 + id: sal_val + - component: Column + children: + - component: Text + text: "Annual Bonus" + variant: caption + - component: Text + text: ${annualBonus} + variant: h4 + - component: Column + children: + - component: Text + text: "Equity Grants" + variant: caption + - component: Text + text: ${equity} + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "🔒 Fetched live from secure HR database" + variant: caption + - component: Text + text: "Verified: ${verifiedAt}" + variant: caption + +sampleData: + employeeName: Dr. Elena Vance + role: Principal Systems Architect + baseSalary: "$215,000" + annualBonus: "$45,000" + equity: "3,500 RSUs" + clearanceLevel: "Level 5 - Confidential" + verifiedAt: "2026-08-13" diff --git a/specification/proposals/templates/examples/section_card.yaml b/specification/proposals/templates/examples/section_card.yaml new file mode 100644 index 0000000000..7511911d03 --- /dev/null +++ b/specification/proposals/templates/examples/section_card.yaml @@ -0,0 +1,52 @@ +version: "0.1" +templateId: SectionCard +description: Standard container card with title, description, optional action, and child components. + +parameters: + title: + type: string + title: Section Title + description: Heading text displayed at the top of the section card. + description: + type: string + title: Section Description + description: Subordinate descriptive text beneath the title. + default: "" + headerAction: + type: child + title: Header Action Component + description: Single child component placed in the right side of the header. + children: + type: children + title: Section Children + description: Child component IDs rendered in the section body. + default: [] + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${title} + variant: h3 + - component: Column + id: action_slot + children: ${headerAction} + - component: Text + text: ${description} + variant: caption + - component: Divider + axis: horizontal + - component: Column + id: body_container + children: ${children} + +sampleData: + title: Protocol Overview + description: High-level summary of the streaming architecture. + children: [] diff --git a/specification/proposals/templates/examples/slot_composition.yaml b/specification/proposals/templates/examples/slot_composition.yaml new file mode 100644 index 0000000000..5145a6675a --- /dev/null +++ b/specification/proposals/templates/examples/slot_composition.yaml @@ -0,0 +1,80 @@ +version: "0.1" +templateId: CompositeSectionCard +description: Collapsible or styled content section card with title, custom header action slot, and children slot. +parameters: + title: + type: string + description: Section header title. + headerAction: + type: child + description: Single child component placed in the right side of the header. + children: + type: children + description: Content components contained within the section body. + default: [] + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${title} + variant: h2 + - component: Column + children: ${headerAction} + - component: Divider + axis: horizontal + - component: Column + children: ${children} + +sampleData: + title: Project Milestones + headerAction: + component: Button + text: Refresh + children: [] + +--- + +version: "0.1" +templateId: TwoColumnLayoutWithHeader +description: Responsive two-column dashboard grid with a hero header, left column, and right column. +parameters: + headerChild: + type: child + description: Component rendered in the full-width header slot. + leftChildren: + type: children + description: Components rendered in the left column. + rightChildren: + type: children + description: Components rendered in the right column. + +layout: + component: Column + children: + - component: Column + children: ${headerChild} + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: start + children: + - component: Column + children: ${leftChildren} + - component: Column + children: ${rightChildren} + +sampleData: + headerChild: + component: Text + text: Executive Operations Dashboard + variant: h1 + leftChildren: [] + rightChildren: [] diff --git a/specification/proposals/templates/examples/team_card.yaml b/specification/proposals/templates/examples/team_card.yaml new file mode 100644 index 0000000000..ec751df98d --- /dev/null +++ b/specification/proposals/templates/examples/team_card.yaml @@ -0,0 +1,68 @@ +version: "0.1" +templateId: TeamCard +description: Card showing team header and unrolled member cards. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team. + members: + type: array + title: Team Members List + description: Array of team member objects. + items: + type: object + title: Team Member + properties: + userId: + type: string + title: User ID + description: Unique identifier of the user. + userName: + type: string + title: User Name + description: Full name of the user. + role: + type: string + title: Role Name + description: Role or title of the user. + default: Member + required: + - userId + - userName + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${teamName} + variant: h3 + - component: Icon + name: person + - component: Divider + axis: horizontal + - component: Column + children: + loop: + param: members + template: UserProfile + +sampleData: + teamName: Antigravity Devs + members: + - userId: usr_101 + userName: Alice Smith + role: Lead Architect + - userId: usr_102 + userName: Bob Jones + role: Senior Engineer + - userId: usr_103 + userName: Charlie Brown + role: Product Manager diff --git a/specification/proposals/templates/examples/team_feedback_board.yaml b/specification/proposals/templates/examples/team_feedback_board.yaml new file mode 100644 index 0000000000..07efe56f33 --- /dev/null +++ b/specification/proposals/templates/examples/team_feedback_board.yaml @@ -0,0 +1,64 @@ +version: "0.1" +templateId: TeamFeedbackBoard +description: Feedback board showing team header and feedback items. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team whose feedback board is being rendered. + feedbacks: + type: array + title: Feedbacks List + description: Array of feedback objects with author, note, and rating. + items: + type: object + title: Feedback Review + properties: + author: + type: string + title: Author Name + description: Full name of the person giving feedback. + note: + type: string + title: Feedback Note + description: Textual comment or recommendation. + rating: + type: number + title: Rating Score + minimum: 1 + maximum: 5 + default: 5 + required: + - author + - note + +layout: + component: Column + children: + - component: Card + child: + component: Row + align: center + children: + - component: Icon + name: mail + - component: Text + text: "Feedback & Retrospective: ${teamName}" + variant: h2 + - component: Column + id: feedbacks_container + children: + loop: + param: feedbacks + template: FeedbackItem + +sampleData: + teamName: Streaming & Protocols Guild + feedbacks: + - author: Dr. Elena Vance + note: Splitting createSurface and updateComponents cleanly unblocked sequential stream processing. + rating: 5 + - author: Marcus Vance + note: Unrolling nested lists statically in Python enables instant frontend mounting with zero runtime recursion. + rating: 5 diff --git a/specification/proposals/templates/examples/team_goal_list.yaml b/specification/proposals/templates/examples/team_goal_list.yaml new file mode 100644 index 0000000000..e2afc5fccc --- /dev/null +++ b/specification/proposals/templates/examples/team_goal_list.yaml @@ -0,0 +1,65 @@ +version: "0.1" +templateId: TeamGoalList +description: List of team goals with header banner. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team whose goals are being listed. + goals: + type: array + title: Goals List + description: Array of goal objects with title, priority, and targetDate. + items: + type: object + title: Goal Definition + properties: + title: + type: string + title: Goal Title + description: Summary title of the objective + priority: + type: enum + title: Priority Level + values: + - High + - Medium + - Low + default: Medium + targetDate: + type: string + title: Target Date + description: Target completion date (YYYY-MM-DD) + required: + - title + +layout: + component: Column + children: + - component: Card + child: + component: Row + align: center + children: + - component: Icon + name: star + - component: Text + text: "Strategic Objectives: ${teamName}" + variant: h2 + - component: Column + id: goals_container + children: + loop: + param: goals + template: GoalItem + +sampleData: + teamName: A2UI Core Team + goals: + - title: Implement bidirectional child/children container resolution + priority: High + targetDate: "2026-07-15" + - title: Upgrade all templates to standard Basic Catalog components + priority: High + targetDate: "2026-07-10" diff --git a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml new file mode 100644 index 0000000000..925788ea82 --- /dev/null +++ b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml @@ -0,0 +1,78 @@ +version: "0.1" +templateId: TeamMemberKnowledgePanel +description: Card showing competency panel with user experience years and completed task count. + +parameters: + userName: + type: string + title: User Display Name + description: The full name of the team member. + role: + type: string + title: Role Name + description: The operational role of the team member. + experienceYears: + type: integer + title: Years of Experience + description: The number of years of professional experience. + minimum: 0 + completedTasks: + type: integer + title: Completed Tasks Count + description: The count of completed tasks or tickets. + minimum: 0 + +layout: + component: Card + child: + component: Column + children: + - component: Row + align: center + children: + - component: Icon + name: check + - component: Text + text: "Competency: ${userName}" + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + children: + - component: Column + align: center + children: + - component: Text + text: Role + variant: caption + - component: Text + text: ${role} + variant: body + - component: Column + align: center + children: + - component: Text + text: Experience + variant: caption + - component: Text + text: "${experienceYears} Yrs" + variant: body + - component: Column + align: center + children: + - component: Text + text: Tasks + variant: caption + - component: Text + text: "${completedTasks} Done" + variant: body + - component: Text + text: Verified Core Contributor + variant: caption + +sampleData: + userName: Alice Smith + role: Systems Architect + experienceYears: 9 + completedTasks: 142 diff --git a/specification/proposals/templates/examples/team_roster.yaml b/specification/proposals/templates/examples/team_roster.yaml new file mode 100644 index 0000000000..1abfb112a7 --- /dev/null +++ b/specification/proposals/templates/examples/team_roster.yaml @@ -0,0 +1,29 @@ +version: "0.1" +templateId: TeamRoster +description: Team directory roster with styled header banner and nested team lists. + +parameters: + directoryTitle: + type: string + title: Directory Title + description: Title text for the team directory. + children: + type: children + title: Team Cards List + description: List of child TeamCard components. + default: [] + +layout: + component: Column + children: + - component: Text + text: ${directoryTitle} + variant: h1 + - component: Divider + axis: horizontal + - component: Column + children: ${children} + +sampleData: + directoryTitle: Global Engineering Directory + children: [] diff --git a/specification/proposals/templates/examples/two_column_layout.yaml b/specification/proposals/templates/examples/two_column_layout.yaml new file mode 100644 index 0000000000..78115c7eaf --- /dev/null +++ b/specification/proposals/templates/examples/two_column_layout.yaml @@ -0,0 +1,40 @@ +version: "0.1" +templateId: TwoColumnLayout +description: Responsive two-column dashboard layout with banner header and split content regions. + +parameters: + headerChild: + type: child + title: Header Component Slot + description: Single child component ID mounted at the top banner. + leftChildren: + type: children + title: Left Column Children + description: Child components placed in the left primary region. + default: [] + rightChildren: + type: children + title: Right Column Children + description: Child components placed in the right secondary region. + default: [] + +layout: + component: Column + children: + - component: Column + children: ${headerChild} + - component: Divider + axis: horizontal + - component: Row + children: + - component: Column + children: ${leftChildren} + - component: Column + children: ${rightChildren} + +sampleData: + headerChild: + component: Text + text: Header Title + leftChildren: [] + rightChildren: [] diff --git a/specification/proposals/templates/examples/user_profile.yaml b/specification/proposals/templates/examples/user_profile.yaml new file mode 100644 index 0000000000..ec722b559e --- /dev/null +++ b/specification/proposals/templates/examples/user_profile.yaml @@ -0,0 +1,38 @@ +version: "0.1" +templateId: UserProfile +description: User profile card displaying avatar, full name, and role. + +parameters: + userId: + type: string + title: User ID + description: Unique user account ID. + userName: + type: string + title: User Name + description: Full name of the user. + role: + type: string + title: Role + description: Job title or role. + default: Member + +layout: + component: Card + child: + component: Column + align: center + children: + - component: Icon + name: person + - component: Text + text: ${userName} + variant: h3 + - component: Text + text: ${role} + variant: caption + +sampleData: + userId: usr_101 + userName: Alice Smith + role: Lead Architect diff --git a/specification/proposals/templates/schema/template_definition.json b/specification/proposals/templates/schema/template_definition.json new file mode 100644 index 0000000000..3c0eb0028f --- /dev/null +++ b/specification/proposals/templates/schema/template_definition.json @@ -0,0 +1,273 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Template Definition", + "description": "Schema defining a parameterized A2UI declarative template authored in YAML with a nested layout tree and synthetic ID generation.", + "type": "object", + "required": ["version", "templateId", "parameters"], + "properties": { + "version": { + "type": "string", + "const": "0.1", + "description": "Specification version of the template definition format. Forced to '0.1'." + }, + "templateId": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_]*$", + "description": "Unique identifier and component tag name of the template." + }, + "description": { + "type": "string", + "description": "Human-readable description of what this template represents." + }, + "parameters": { + "type": "object", + "description": "Mapping of parameter names to their explicit A2UI type definitions.", + "additionalProperties": { + "$ref": "#/$defs/ParameterDefinition" + } + }, + "requiredParameters": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of parameter names that are required when invoking the template." + }, + "layout": { + "$ref": "#/$defs/NestedTemplateNode", + "description": "Root component of the nested layout tree." + }, + "components": { + "type": "array", + "description": "Optional flattened component list for internal graph representation.", + "items": { + "$ref": "#/$defs/TemplateComponent" + } + }, + "sampleData": { + "type": "object", + "description": "Sample parameter values used for previewing and testing template inflation." + } + }, + "anyOf": [ + {"required": ["layout"]}, + {"required": ["components"]} + ], + "additionalProperties": false, + "$defs": { + "ParameterDefinition": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "enum", + "object", + "array", + "child", + "children", + "action" + ], + "description": "A2UI semantic parameter type." + }, + "title": { + "type": "string", + "description": "Short human-readable title of the parameter." + }, + "description": { + "type": "string", + "description": "Detailed explanation of the parameter's purpose and usage." + }, + "default": { + "description": "Default fallback value if not specified." + }, + "values": { + "type": "array", + "items": {"type": "string"}, + "description": "Allowed string values when type is 'enum'." + }, + "items": { + "description": "Item type definition when type is 'array'. Can be an A2UI type name or nested ParameterDefinition." + }, + "properties": { + "type": "object", + "description": "Property definitions when type is 'object'.", + "additionalProperties": { + "$ref": "#/$defs/ParameterDefinition" + } + }, + "required": { + "anyOf": [ + { + "type": "boolean", + "description": "Whether this parameter is required when instantiating the template." + }, + { + "type": "array", + "items": {"type": "string"}, + "description": "List of required property names for object types." + } + ] + }, + "minimum": { + "type": "number", + "description": "Minimum value constraint for numeric parameters." + }, + "maximum": { + "type": "number", + "description": "Maximum value constraint for numeric parameters." + } + }, + "additionalProperties": false + }, + "ParamReference": { + "type": "object", + "required": ["param"], + "properties": { + "param": { + "type": "string", + "description": "Name or dot-separated path of the template parameter to substitute." + }, + "template": { + "type": "string", + "description": "Child template identifier when mapping an array parameter to child component instances." + }, + "default": { + "description": "Fallback value if the parameter path resolves to None." + } + }, + "additionalProperties": false + }, + "ConcatExpression": { + "type": "object", + "required": ["concat"], + "properties": { + "concat": { + "type": "array", + "description": "List of literal values and parameter references to concatenate into a string.", + "items": { + "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] + } + } + }, + "additionalProperties": false + }, + "FormatExpression": { + "type": "object", + "required": ["format"], + "properties": { + "format": { + "type": "string", + "description": "Format string containing {key} placeholders." + }, + "args": { + "type": "object", + "description": "Mapping of placeholder keys to values or parameter references.", + "additionalProperties": { + "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] + } + } + }, + "additionalProperties": false + }, + "LoopDefinition": { + "type": "object", + "required": ["param"], + "anyOf": [ + {"required": ["template"]}, + {"required": ["item"]} + ], + "properties": { + "param": { + "type": "string", + "description": "Parameter name holding the list/array data to iterate over." + }, + "template": { + "type": "string", + "description": "Identifier of a named template to instantiate for each item." + }, + "item": { + "$ref": "#/$defs/NestedTemplateNode", + "description": "Self-contained inline component layout to instantiate for each item." + }, + "as": { + "type": "string", + "description": "Optional variable scope alias for accessing item fields in expressions." + } + }, + "additionalProperties": false + }, + "TemplateValue": { + "description": "A literal value, parameter substitution, or expression in a template component property.", + "anyOf": [ + {"type": "string"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "null"}, + {"$ref": "#/$defs/ParamReference"}, + {"$ref": "#/$defs/ConcatExpression"}, + {"$ref": "#/$defs/FormatExpression"}, + {"type": "array"}, + {"type": "object"} + ] + }, + "NestedTemplateNode": { + "type": "object", + "required": ["component"], + "properties": { + "id": { + "type": "string", + "description": "Optional explicit component ID within the template." + }, + "component": { + "type": "string", + "description": "Catalog component tag name." + }, + "child": { + "anyOf": [ + {"$ref": "#/$defs/NestedTemplateNode"}, + {"$ref": "#/$defs/TemplateValue"} + ] + }, + "children": { + "anyOf": [ + { + "type": "object", + "required": ["loop"], + "properties": { + "loop": {"$ref": "#/$defs/LoopDefinition"} + }, + "additionalProperties": false + }, + {"type": "array"}, + {"$ref": "#/$defs/ParamReference"}, + {"type": "string"} + ] + } + }, + "additionalProperties": { + "$ref": "#/$defs/TemplateValue" + } + }, + "TemplateComponent": { + "type": "object", + "required": ["id", "component"], + "properties": { + "id": { + "type": "string", + "description": "Unique component ID within the template." + }, + "component": { + "type": "string", + "description": "Catalog component tag name." + } + }, + "additionalProperties": { + "$ref": "#/$defs/TemplateValue" + } + } + } +} From 5950f4c920e373a495a859eb9898c2dc7c69a82c Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 10:57:04 +0930 Subject: [PATCH 02/26] style(templates): add license headers and format specification files --- specification/proposals/templates/README.md | 164 +++++++++++++----- .../templates/examples/basic_cards.yaml | 15 +- .../templates/examples/feedback_item.yaml | 14 ++ .../templates/examples/goal_item.yaml | 14 ++ .../templates/examples/nested_lists.yaml | 15 +- .../templates/examples/salary_card.yaml | 14 ++ .../templates/examples/section_card.yaml | 14 ++ .../templates/examples/slot_composition.yaml | 15 +- .../templates/examples/team_card.yaml | 14 ++ .../examples/team_feedback_board.yaml | 14 ++ .../templates/examples/team_goal_list.yaml | 14 ++ .../examples/team_member_knowledge_panel.yaml | 14 ++ .../templates/examples/team_roster.yaml | 14 ++ .../templates/examples/two_column_layout.yaml | 14 ++ .../templates/examples/user_profile.yaml | 14 ++ .../templates/schema/template_definition.json | 15 +- 16 files changed, 322 insertions(+), 56 deletions(-) diff --git a/specification/proposals/templates/README.md b/specification/proposals/templates/README.md index a53b5c487b..9064c6c5eb 100644 --- a/specification/proposals/templates/README.md +++ b/specification/proposals/templates/README.md @@ -2,11 +2,12 @@ ## Abstract -This document defines the authoritative specification for the **A2UI Templates System**. +This document defines the authoritative specification for the **A2UI Templates System**. Templates provide a declarative, human-readable mechanism for authoring reusable UI subtrees in nested YAML or via native programmatic render functions. At generation time, an agent or Large Language Model (LLM) outputs compact, high-level template invocations (such as `UserProfile("u1", "Alice")` or `PayrollSummary("Eng", true)`). The backend SDK expands these invocations into standard, flat A2UI Basic Catalog components in a single synchronous pass before emitting standard A2UI protocol messages (`updateComponents`) to the client. This architecture achieves three fundamental objectives: + 1. **Human Readability**: Layouts are authored as clean, natural tree hierarchies in YAML or native language code, eliminating artificial IDs and flat array boilerplate. 2. **Token Efficiency & Context Optimization**: LLMs emit concise function-like signatures rather than verbose, deeply nested UI component trees, saving prompt context and generation tokens. 3. **Zero Client Overhead**: Client renderers (@a2ui/react, @a2ui/lit, @a2ui/angular, @a2ui/flutter) implement only standard Basic Catalog primitives (`Card`, `Column`, `Row`, `Text`, `Divider`, `Icon`, `Button`). No custom client widgets, dynamic code deployment, or renderer plugins are required. @@ -70,10 +71,11 @@ The template engine operates entirely server-side within the A2UI Agent SDK: A2UI defines three template modalities: ### A. Declarative Static Templates (YAML) + Declarative static templates define immutable layout trees authored in YAML files. All parameter placeholders are populated directly from arguments provided by the model or caller. ```yaml -version: "0.1" +version: '0.1' templateId: UserProfile description: Standard identity card for team members. parameters: @@ -104,7 +106,9 @@ layout: ``` ### B. Dynamic Templates: Data-Binding Mode (`resolver + layout`) -Data-binding dynamic templates decouple public model parameters from private or live backend data. + +Data-binding dynamic templates decouple public model parameters from private or live backend data. + - **Public Surface**: The model only sees and outputs high-level lookup parameters (e.g. `employeeId: "emp_101"`). - **Server Resolver**: At expansion time, a backend callback executes (e.g., querying an internal HR database or API) and produces a data dictionary. - **Layout Inflation**: The resolved dictionary is merged with the caller's arguments and applied to an underlying static YAML layout. @@ -122,13 +126,14 @@ dynamic_salary = DynamicTemplate( **Security Rationale**: Confidential numbers (e.g., executive salaries, PII) never enter the model's prompt context window or inference transcript. ### C. Dynamic Templates: Programmatic Render Mode (`render_fn`) + Programmatic dynamic templates bypass static YAML layouts entirely. Instead, a developer writes a native function in the host language (Python, Dart, etc.) that accepts parameters and directly returns a UI component tree. ```python def render_payroll_summary(department: str = "Engineering", includeBonus: bool = True) -> dict: total_base = sum(e.salary for e in db.get_dept(department)) total_bonus = sum(e.bonus for e in db.get_dept(department)) - + rows = [] for emp in db.get_dept(department): cols = [ @@ -161,6 +166,7 @@ payroll_tmpl = DynamicTemplate( ``` **Advantages**: + - Full host language power: arbitrary `for` loops, mathematical calculations, currency formatting, conditionals, and recursion. - Polymorphic return values: accepts raw dictionaries/maps, dataclasses, or typesafe fluent builder objects (`Card()`, `Column()`). @@ -172,20 +178,22 @@ A critical question in template design is: **When does the template engine unrol ### Comparison Matrix -| Dimension | Server-Side Template Unrolling | Client-Side DataModel Loop | -| :--- | :--- | :--- | -| **Execution Point** | Backend SDK (`TemplateProcessor`) during template expansion. | Client Renderer runtime during paint/state updates. | -| **Data Source** | Static YAML literals, LLM invocation arguments, or server resolver output. | Client `DataModel` tree (e.g. `/session/cart/items`). | -| **Component Output** | Emits $N$ concrete Basic Catalog component nodes with synthesized unique IDs. | Emits a single repeater/collection component with a `DataBinding` path. | -| **Client Requirement** | Zero. Standard Basic Catalog renderers render it like any other static UI. | Requires client-side repeater support or SDK session re-renders. | -| **Dynamic Mutation** | Immutable once generated unless the agent emits a new `updateComponents` message. | Reactively re-renders when the client data model updates (e.g. via button events). | +| Dimension | Server-Side Template Unrolling | Client-Side DataModel Loop | +| :--------------------- | :-------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- | +| **Execution Point** | Backend SDK (`TemplateProcessor`) during template expansion. | Client Renderer runtime during paint/state updates. | +| **Data Source** | Static YAML literals, LLM invocation arguments, or server resolver output. | Client `DataModel` tree (e.g. `/session/cart/items`). | +| **Component Output** | Emits $N$ concrete Basic Catalog component nodes with synthesized unique IDs. | Emits a single repeater/collection component with a `DataBinding` path. | +| **Client Requirement** | Zero. Standard Basic Catalog renderers render it like any other static UI. | Requires client-side repeater support or SDK session re-renders. | +| **Dynamic Mutation** | Immutable once generated unless the agent emits a new `updateComponents` message. | Reactively re-renders when the client data model updates (e.g. via button events). | ### Example 1: Server-Side Template Loop Unrolling + A template author specifies a loop over a parameter array: #### Template Definition: + ```yaml -version: "0.1" +version: '0.1' templateId: TeamGoalList parameters: teamName: {type: string} @@ -196,7 +204,7 @@ layout: component: Column children: - component: Text - text: "Objectives: ${teamName}" + text: 'Objectives: ${teamName}' variant: h2 - component: Column children: @@ -215,6 +223,7 @@ layout: ``` #### Model Invocation: + ```text root = TeamGoalList("Core Team", [ {title: "Ship Protocol", priority: "High"}, @@ -223,28 +232,71 @@ root = TeamGoalList("Core Team", [ ``` #### Expanded Output Sent to Client: + The engine unrolls the loop into concrete, synthetic child components: + ```json [ {"id": "root", "component": "Card", "child": "root_child_column"}, - {"id": "root_child_column", "component": "Column", "children": ["root_child_column_children_0_text", "root_child_column_children_1_column"]}, - {"id": "root_child_column_children_0_text", "component": "Text", "text": "Objectives: Core Team", "variant": "h2"}, - {"id": "root_child_column_children_1_column", "component": "Column", "children": [ - "root_child_column_children_1_column_goals_0_row", - "root_child_column_children_1_column_goals_1_row" - ]}, - {"id": "root_child_column_children_1_column_goals_0_row", "component": "Row", "justify": "spaceBetween", "children": [ - "root_child_column_children_1_column_goals_0_row_children_0_text", - "root_child_column_children_1_column_goals_0_row_children_1_text" - ]}, - {"id": "root_child_column_children_1_column_goals_0_row_children_0_text", "component": "Text", "text": "Ship Protocol"}, - {"id": "root_child_column_children_1_column_goals_0_row_children_1_text", "component": "Text", "text": "High", "variant": "caption"}, - {"id": "root_child_column_children_1_column_goals_1_row", "component": "Row", "justify": "spaceBetween", "children": [ - "root_child_column_children_1_column_goals_1_row_children_0_text", - "root_child_column_children_1_column_goals_1_row_children_1_text" - ]}, - {"id": "root_child_column_children_1_column_goals_1_row_children_0_text", "component": "Text", "text": "Write Tests"}, - {"id": "root_child_column_children_1_column_goals_1_row_children_1_text", "component": "Text", "text": "Medium", "variant": "caption"} + { + "id": "root_child_column", + "component": "Column", + "children": ["root_child_column_children_0_text", "root_child_column_children_1_column"] + }, + { + "id": "root_child_column_children_0_text", + "component": "Text", + "text": "Objectives: Core Team", + "variant": "h2" + }, + { + "id": "root_child_column_children_1_column", + "component": "Column", + "children": [ + "root_child_column_children_1_column_goals_0_row", + "root_child_column_children_1_column_goals_1_row" + ] + }, + { + "id": "root_child_column_children_1_column_goals_0_row", + "component": "Row", + "justify": "spaceBetween", + "children": [ + "root_child_column_children_1_column_goals_0_row_children_0_text", + "root_child_column_children_1_column_goals_0_row_children_1_text" + ] + }, + { + "id": "root_child_column_children_1_column_goals_0_row_children_0_text", + "component": "Text", + "text": "Ship Protocol" + }, + { + "id": "root_child_column_children_1_column_goals_0_row_children_1_text", + "component": "Text", + "text": "High", + "variant": "caption" + }, + { + "id": "root_child_column_children_1_column_goals_1_row", + "component": "Row", + "justify": "spaceBetween", + "children": [ + "root_child_column_children_1_column_goals_1_row_children_0_text", + "root_child_column_children_1_column_goals_1_row_children_1_text" + ] + }, + { + "id": "root_child_column_children_1_column_goals_1_row_children_0_text", + "component": "Text", + "text": "Write Tests" + }, + { + "id": "root_child_column_children_1_column_goals_1_row_children_1_text", + "component": "Text", + "text": "Medium", + "variant": "caption" + } ] ``` @@ -253,16 +305,18 @@ The engine unrolls the loop into concrete, synthetic child components: ## 4. Relationship to the A2UI Data Model Templates and the A2UI Data Model operate in complementary scopes: + - **Templates** expand during **server-side inference / response formulation**. - **Data Model** operates during **client-side runtime interaction & reactivity**. Templates interact with the client Data Model in three distinct patterns: ### Pattern 1: Dynamic Path Plumbing (Path Interpolation) + Templates can accept path prefixes or IDs as parameters, constructing reactive client-side binding paths: ```yaml -version: "0.1" +version: '0.1' templateId: BoundLiveMetric parameters: metricKey: {type: string} @@ -276,10 +330,11 @@ layout: text: ${label} - component: Text text: - path: "/system/metrics/${metricKey}/currentValue" + path: '/system/metrics/${metricKey}/currentValue' ``` **Expanded Output:** + ```json { "id": "root_val", @@ -289,19 +344,23 @@ layout: } } ``` -*Client Behavior*: The client renderer attaches a live subscriber to `/system/metrics/cpuLoad/currentValue`. When telemetry updates arrive via `updateDataModel`, the component automatically re-renders without server roundtrips. + +_Client Behavior_: The client renderer attaches a live subscriber to `/system/metrics/cpuLoad/currentValue`. When telemetry updates arrive via `updateDataModel`, the component automatically re-renders without server roundtrips. ### Pattern 2: Direct `DataBinding` Passthrough + When a template parameter has `type: object`, an agent or caller can pass an explicit A2UI binding object: #### Model Invocation: + ```text root = MetricCard("Memory Used", {path: "/device/mem_percent"}) ``` #### Template Layout: + ```yaml -version: "0.1" +version: '0.1' templateId: MetricCard parameters: title: {type: string} @@ -317,6 +376,7 @@ layout: **Expanded Output:** Because `${val}` is an exact-match substitution, the dictionary type is strictly preserved: + ```json { "component": "Text", @@ -325,13 +385,14 @@ Because `${val}` is an exact-match substitution, the dictionary type is strictly ``` ### Pattern 3: Hardcoded Session State References + Templates can embed constant client bindings for global session preferences: ```yaml layout: component: Text text: - path: "/session/currentUser/displayName" + path: '/session/currentUser/displayName' ``` --- @@ -341,7 +402,9 @@ layout: To make templates discoverable and invoke-able by LLMs, the backend converts registered templates into a **Synthetic Component Catalog**. ### The Catalog Synthesis Algorithm + When `TemplateProcessor(templates)` is initialized: + 1. It copies the base catalog (e.g. Basic Catalog v0.9.1). 2. For each registered `StaticTemplate` and `DynamicTemplate`: - It registers a new component definition under `components[templateId]`. @@ -358,6 +421,7 @@ When `TemplateProcessor(templates)` is initialized: 3. The synthetic catalog is passed to `PromptGenerator` (Express, Elemental, Atom, Direct JSON), which automatically writes the syntax rules and documentation into the system prompt. ### Surface Isolation Boundary + ``` [LLM Agent] <---> [Synthetic Catalog (Templates + Primitives)] <---> [TemplateProcessor] | @@ -366,6 +430,7 @@ When `TemplateProcessor(templates)` is initialized: v [Client Renderer] <---------------- (Standard Basic Catalog Only) <----------+ ``` + The synthetic catalog is an ephemeral compile-time construct. The client renderer is completely unaware that templates exist. --- @@ -375,21 +440,27 @@ The synthetic catalog is an ephemeral compile-time construct. The client rendere The template substitution engine replaces expressions according to strict typing and evaluation rules. ### Rule 1: Exact Match Substitution (Type Preserving) + If a string field value exactly matches the parameter token regex `^\$\{([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}$`: + - The entire field is replaced by the native runtime value of the parameter. - **Integers remain integers**: `level: ${userLevel}` $\rightarrow$ `level: 4` (not `"4"`). - **Booleans remain booleans**: `checked: ${isActive}` $\rightarrow$ `checked: true` (not `"true"`). - **Objects and Arrays remain native structures**: `items: ${memberList}` $\rightarrow$ `items: [{...}, {...}]`. ### Rule 2: Embedded String Interpolation + If a parameter token appears alongside other characters (e.g. `"Hello, ${userName}!"` or `"${dept} - ${code}"`): + - All tokens are replaced by their string representations. - Evaluates to a native string. ### Rule 3: Deep Dot-Notation for Structured Objects + Templates natively support deep dot-notation paths on object parameters: #### Invocation: + ```text root = AccountCard({ account: { @@ -400,8 +471,9 @@ root = AccountCard({ ``` #### Template Layout: + ```yaml -version: "0.1" +version: '0.1' templateId: AccountCard parameters: account: {type: object} @@ -409,10 +481,11 @@ layout: component: Card child: component: Text - text: "${account.profile.name} (${account.profile.tier})" + text: '${account.profile.name} (${account.profile.tier})' ``` **Expanded Output:** + ```json { "component": "Text", @@ -421,19 +494,24 @@ layout: ``` ### Rule 4: String Format AST Expressions + For complex string formatting across multiple languages: + ```yaml -format: "Level {lvl} ({pts} points)" +format: 'Level {lvl} ({pts} points)' args: lvl: ${level} pts: ${experiencePoints} ``` ### Rule 5: Token Escaping + To emit a literal `${foo}` string without triggering parameter substitution, escape the leading dollar sign with a backslash: + - `\${keep_literal}` $\rightarrow$ evaluates to literal `"${keep_literal}"`. ### Rule 6: Missing Values & Defaults + - If an argument is omitted but has a declared `default` in its parameter schema, the `default` is used. - If an argument is omitted, has no `default`, and is not required: the property is omitted from the synthesized component (it is not emitted as `null`). - If an argument is required and missing: expansion raises a `TemplateParameterError`. @@ -482,8 +560,9 @@ This guarantees that two instances of the same template (`card_1` and `card_2`) Templates can define structural containers that receive caller-provided components. ### Example: `SectionCard` Container + ```yaml -version: "0.1" +version: '0.1' templateId: SectionCard parameters: title: {type: string} @@ -511,6 +590,7 @@ layout: ``` #### Model Invocation: + ```text root = SectionCard( "Project Status", @@ -529,6 +609,7 @@ The template engine replaces `${headerAction}` with the synthesized single child ## 9. Error Handling, Cycle Guards & Safety ### Recursion & Circular Reference Detection + Templates can invoke other templates. However, circular references (`A` invokes `B`, which invokes `A`) must be caught immediately to prevent stack overflow. - **Call Stack Tracking**: The `TemplateProcessor` maintains an active `_call_stack: Set[str]` throughout expansion. @@ -536,6 +617,7 @@ Templates can invoke other templates. However, circular references (`A` invokes - **Maximum Depth Guard**: An absolute limit (`MAX_EXPANSION_DEPTH = 32`) enforces termination even in non-identical runaway recursion. ### Standard Error Hierarchy + 1. `TemplateNotFoundError`: Attempted to expand a `templateId` not present in the catalog. 2. `TemplateParameterError`: Missing required parameter or invalid parameter type. 3. `TemplateCycleError`: Circular reference detected during expansion. @@ -558,6 +640,7 @@ To streamline template maintenance, multiple templates can be defined in a singl To ensure robust backward compatibility as template syntax and AST features evolve across future protocol releases, every template definition must declare a top-level `version` field. ### Versioning Rules: + 1. **Mandatory Version Field**: Every YAML document (and each document within multi-doc streams separated by `---`) must include `version: "0.1"`. 2. **Strict Schema Validation**: The canonical JSON schema enforces `"version": {"type": "string", "const": "0.1"}`. Attempting to inflate a template with an unsupported version raises an explicit `TemplateVersionError` / `ValueError`. 3. **Future Extensibility**: As new template language capabilities are standardized (e.g. conditional slot rendering, client-expanded template directives), new version identifiers (such as `"0.2"` or `"1.0"`) allow engines to select the appropriate parser/evaluator without breaking legacy templates. @@ -579,4 +662,3 @@ An implementation of the A2UI Template Engine in any language (Python, Dart, Typ - [ ] **Cycle and Recursion Safeguards**: Catches circular references and depth violations with explicit exceptions. - [ ] **Synthetic Catalog Generation**: Generates valid A2UI JSON schema catalogs matching input parameter definitions. - [ ] **Polymorphic Dynamic Templates**: Supports both resolver-based static bindings and programmatic render functions. - diff --git a/specification/proposals/templates/examples/basic_cards.yaml b/specification/proposals/templates/examples/basic_cards.yaml index ab6b8bfc8a..d9baa44ea0 100644 --- a/specification/proposals/templates/examples/basic_cards.yaml +++ b/specification/proposals/templates/examples/basic_cards.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: BasicUserProfile description: Standard identity card for team members. @@ -34,7 +48,6 @@ sampleData: role: Lead Architect --- - version: "0.1" templateId: BasicMetricCard description: Metric display card supporting both literal values and dynamic data model bindings. diff --git a/specification/proposals/templates/examples/feedback_item.yaml b/specification/proposals/templates/examples/feedback_item.yaml index bb170c2333..a5baad6a6e 100644 --- a/specification/proposals/templates/examples/feedback_item.yaml +++ b/specification/proposals/templates/examples/feedback_item.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: FeedbackItem description: Card showing feedback note, author, and rating. diff --git a/specification/proposals/templates/examples/goal_item.yaml b/specification/proposals/templates/examples/goal_item.yaml index a9009555b0..06b3d4d3ae 100644 --- a/specification/proposals/templates/examples/goal_item.yaml +++ b/specification/proposals/templates/examples/goal_item.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: GoalItem description: Card showing an individual objective with priority, title, target date, and action button. diff --git a/specification/proposals/templates/examples/nested_lists.yaml b/specification/proposals/templates/examples/nested_lists.yaml index 8afd3d5bab..80d8a195a5 100644 --- a/specification/proposals/templates/examples/nested_lists.yaml +++ b/specification/proposals/templates/examples/nested_lists.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: NestedTeamRoster description: Team directory roster with styled header banner and nested team lists. @@ -37,7 +51,6 @@ sampleData: role: Streaming Lead --- - version: "0.1" templateId: NestedTeamCard description: Team container card displaying member cards. diff --git a/specification/proposals/templates/examples/salary_card.yaml b/specification/proposals/templates/examples/salary_card.yaml index f4b847e746..b1959b71b8 100644 --- a/specification/proposals/templates/examples/salary_card.yaml +++ b/specification/proposals/templates/examples/salary_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: SalaryCard description: Layout card for employee compensation package with security verification badge. diff --git a/specification/proposals/templates/examples/section_card.yaml b/specification/proposals/templates/examples/section_card.yaml index 7511911d03..34ade454b4 100644 --- a/specification/proposals/templates/examples/section_card.yaml +++ b/specification/proposals/templates/examples/section_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: SectionCard description: Standard container card with title, description, optional action, and child components. diff --git a/specification/proposals/templates/examples/slot_composition.yaml b/specification/proposals/templates/examples/slot_composition.yaml index 5145a6675a..c269f1992c 100644 --- a/specification/proposals/templates/examples/slot_composition.yaml +++ b/specification/proposals/templates/examples/slot_composition.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: CompositeSectionCard description: Collapsible or styled content section card with title, custom header action slot, and children slot. @@ -40,7 +54,6 @@ sampleData: children: [] --- - version: "0.1" templateId: TwoColumnLayoutWithHeader description: Responsive two-column dashboard grid with a hero header, left column, and right column. diff --git a/specification/proposals/templates/examples/team_card.yaml b/specification/proposals/templates/examples/team_card.yaml index ec751df98d..d7f47ab03f 100644 --- a/specification/proposals/templates/examples/team_card.yaml +++ b/specification/proposals/templates/examples/team_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamCard description: Card showing team header and unrolled member cards. diff --git a/specification/proposals/templates/examples/team_feedback_board.yaml b/specification/proposals/templates/examples/team_feedback_board.yaml index 07efe56f33..d7f874dac5 100644 --- a/specification/proposals/templates/examples/team_feedback_board.yaml +++ b/specification/proposals/templates/examples/team_feedback_board.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamFeedbackBoard description: Feedback board showing team header and feedback items. diff --git a/specification/proposals/templates/examples/team_goal_list.yaml b/specification/proposals/templates/examples/team_goal_list.yaml index e2afc5fccc..5a7c092c66 100644 --- a/specification/proposals/templates/examples/team_goal_list.yaml +++ b/specification/proposals/templates/examples/team_goal_list.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamGoalList description: List of team goals with header banner. diff --git a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml index 925788ea82..4d68cd0258 100644 --- a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml +++ b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamMemberKnowledgePanel description: Card showing competency panel with user experience years and completed task count. diff --git a/specification/proposals/templates/examples/team_roster.yaml b/specification/proposals/templates/examples/team_roster.yaml index 1abfb112a7..f0a96d0c0c 100644 --- a/specification/proposals/templates/examples/team_roster.yaml +++ b/specification/proposals/templates/examples/team_roster.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamRoster description: Team directory roster with styled header banner and nested team lists. diff --git a/specification/proposals/templates/examples/two_column_layout.yaml b/specification/proposals/templates/examples/two_column_layout.yaml index 78115c7eaf..c2d1866885 100644 --- a/specification/proposals/templates/examples/two_column_layout.yaml +++ b/specification/proposals/templates/examples/two_column_layout.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TwoColumnLayout description: Responsive two-column dashboard layout with banner header and split content regions. diff --git a/specification/proposals/templates/examples/user_profile.yaml b/specification/proposals/templates/examples/user_profile.yaml index ec722b559e..517215afa2 100644 --- a/specification/proposals/templates/examples/user_profile.yaml +++ b/specification/proposals/templates/examples/user_profile.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: UserProfile description: User profile card displaying avatar, full name, and role. diff --git a/specification/proposals/templates/schema/template_definition.json b/specification/proposals/templates/schema/template_definition.json index 3c0eb0028f..653ad54c7d 100644 --- a/specification/proposals/templates/schema/template_definition.json +++ b/specification/proposals/templates/schema/template_definition.json @@ -47,10 +47,7 @@ "description": "Sample parameter values used for previewing and testing template inflation." } }, - "anyOf": [ - {"required": ["layout"]}, - {"required": ["components"]} - ], + "anyOf": [{"required": ["layout"]}, {"required": ["components"]}], "additionalProperties": false, "$defs": { "ParameterDefinition": { @@ -176,10 +173,7 @@ "LoopDefinition": { "type": "object", "required": ["param"], - "anyOf": [ - {"required": ["template"]}, - {"required": ["item"]} - ], + "anyOf": [{"required": ["template"]}, {"required": ["item"]}], "properties": { "param": { "type": "string", @@ -227,10 +221,7 @@ "description": "Catalog component tag name." }, "child": { - "anyOf": [ - {"$ref": "#/$defs/NestedTemplateNode"}, - {"$ref": "#/$defs/TemplateValue"} - ] + "anyOf": [{"$ref": "#/$defs/NestedTemplateNode"}, {"$ref": "#/$defs/TemplateValue"}] }, "children": { "anyOf": [ From 4c0426714a5eabb1fe9fbf6f37fb7191564f9595 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:11:36 +0930 Subject: [PATCH 03/26] feat(templates): specify name, id, catalogs, and imports in template definition specification --- specification/proposals/templates/README.md | 105 ++++++++++++++---- .../templates/examples/basic_cards.yaml | 8 +- .../templates/examples/feedback_item.yaml | 4 +- .../templates/examples/goal_item.yaml | 4 +- .../templates/examples/nested_lists.yaml | 8 +- .../templates/examples/salary_card.yaml | 4 +- .../templates/examples/section_card.yaml | 4 +- .../templates/examples/slot_composition.yaml | 8 +- .../templates/examples/team_card.yaml | 4 +- .../examples/team_feedback_board.yaml | 4 +- .../templates/examples/team_goal_list.yaml | 4 +- .../examples/team_member_knowledge_panel.yaml | 4 +- .../templates/examples/team_roster.yaml | 4 +- .../templates/examples/two_column_layout.yaml | 4 +- .../templates/examples/user_profile.yaml | 4 +- .../templates/schema/template_definition.json | 54 ++++++++- 16 files changed, 187 insertions(+), 40 deletions(-) diff --git a/specification/proposals/templates/README.md b/specification/proposals/templates/README.md index 9064c6c5eb..e39b3984ac 100644 --- a/specification/proposals/templates/README.md +++ b/specification/proposals/templates/README.md @@ -76,7 +76,9 @@ Declarative static templates define immutable layout trees authored in YAML file ```yaml version: '0.1' -templateId: UserProfile +name: UserProfile +catalogs: + - 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json' description: Standard identity card for team members. parameters: userId: @@ -116,7 +118,8 @@ Data-binding dynamic templates decouple public model parameters from private or ```python # Server Registration dynamic_salary = DynamicTemplate( - template_id="EmployeeSalaryCard", + name="EmployeeSalaryCard", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], resolver=fetch_private_compensation, layout=salary_yaml_layout, description="Verified compensation card. Pass only employeeId.", @@ -159,7 +162,8 @@ def render_payroll_summary(department: str = "Engineering", includeBonus: bool = } payroll_tmpl = DynamicTemplate( - template_id="PayrollSummary", + name="PayrollSummary", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], render=render_payroll_summary, description="Dynamic payroll calculation matrix.", ) @@ -194,7 +198,9 @@ A template author specifies a loop over a parameter array: ```yaml version: '0.1' -templateId: TeamGoalList +name: TeamGoalList +catalogs: + - 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json' parameters: teamName: {type: string} goals: {type: array} @@ -317,7 +323,9 @@ Templates can accept path prefixes or IDs as parameters, constructing reactive c ```yaml version: '0.1' -templateId: BoundLiveMetric +name: BoundLiveMetric +catalogs: + - 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json' parameters: metricKey: {type: string} label: {type: string} @@ -397,17 +405,65 @@ layout: --- -## 5. The Synthetic Catalog System +## 5. Catalog Declaration, Validation & Multi-Catalog Disambiguation + +Templates must declare which catalog(s) they are authored against via the top-level `catalogs` property. + +### A. Mandatory Explicit Catalogs (No Basic Catalog Defaults) + +The template engine enforces strict decoupling from any specific catalog: + +- **No Hardcoded Defaults**: `TemplateProcessor` does not default to the Basic Catalog or any other specific catalog. +- **Explicit Catalog Requirement**: All catalogs required by registered templates must be explicitly provided to the processor upon initialization (e.g. via `catalogs={...}`). +- **Static Integrity Validation**: At registration time, the engine validates that every template's declared `catalogs` can be resolved against the provided catalog registry, and every primitive component referenced in `layout` exists within those catalogs with valid properties. + +### B. Prefix-Free Automatic Component Resolution + +In layout trees, authors write clean, standard component names (`Card`, `Text`, `HeartRateGraph`) without prefixes or namespace boilerplate: + +- **Unique Match (Standard Case)**: If component `Foo` exists in exactly one declared catalog, it automatically resolves to that catalog. +- **Name Collision**: If two declared catalogs define `Foo`, the author can specify `catalogId` on that specific component in the layout to disambiguate. If omitted, the engine raises an `AmbiguousComponentError`. +- **Unknown Component**: If `Foo` is found in zero declared catalogs, the engine raises an `UnknownComponentError`. + +### C. Protocol Version Rules: v0.9 vs. v1.0 + +The template engine tailors its expansion and output depending on the target protocol version: + +| Dimension | A2UI v0.9 / v0.9.1 Behavior | A2UI v1.0 Behavior | +| :---------------------------- | :------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------- | +| **Surface Catalog Model** | Single catalog per surface (`createSurface.catalogId` required). | Mixable catalogs (`createSurface.catalogId` optional default). | +| **Multi-Catalog Templates** | **Rejected**: If a template declares multiple catalogs, unrolling onto a v0.9 surface raises `CatalogCompatibilityError`. | **Supported**: Expanded seamlessly across mixed catalogs. | +| **Surface Validation** | Template's resolved catalog MUST match `createSurface.catalogId`. | All catalogs referenced by the template must be present in surface `supportedCatalogIds`. | +| **Expanded Component Output** | `catalogId` is **never** emitted on expanded component dicts (illegal property in v0.9). | `catalogId` is automatically injected on any component whose catalog differs from surface default. | + +--- + +## 6. Sub-Template Imports & Global Identity + +Templates decouple their local invocation name from their optional global identity: + +- **`name`** (Required): Component tag name used in layout trees and LLM prompts (`^[A-Za-z][A-Za-z0-9_]*$`). `templateId` is accepted as a backwards-compatible alias. +- **`id`** (Optional): Globally unique URI or URN string (e.g. `https://company.org/templates/card/v1.json`) for package distribution and version pinning. +- **`imports`** (Optional): List of global template IDs, or dictionary mapping local aliases to global template IDs (`imports: { VendorCard: "https://vendor.com/card.json" }`). + +### Usage Tiers + +1. **Tier 1 (Simple / Local)**: Templates omit `id` and `imports`. Sibling templates in the same file or registry resolve each other directly by `name`. +2. **Tier 2 (Modular / Distributed)**: Templates declare `id` and import external sub-templates by `id` or alias via `imports`. + +--- + +## 7. The Synthetic Catalog System To make templates discoverable and invoke-able by LLMs, the backend converts registered templates into a **Synthetic Component Catalog**. ### The Catalog Synthesis Algorithm -When `TemplateProcessor(templates)` is initialized: +When `TemplateProcessor(templates, catalogs)` is initialized: -1. It copies the base catalog (e.g. Basic Catalog v0.9.1). +1. It indexes all provided catalogs. 2. For each registered `StaticTemplate` and `DynamicTemplate`: - - It registers a new component definition under `components[templateId]`. + - It registers a new component definition under `components[template.name]`. - It maps semantic parameter definitions to JSON schema properties: - `string` $\rightarrow$ `{"type": "string"}` - `number` / `integer` $\rightarrow$ `{"type": "number"}` / `{"type": "integer"}` @@ -428,14 +484,14 @@ When `TemplateProcessor(templates)` is initialized: (Expands to Primitives) | v -[Client Renderer] <---------------- (Standard Basic Catalog Only) <----------+ +[Client Renderer] <---------------- (Standard Catalog Components) <---------+ ``` The synthetic catalog is an ephemeral compile-time construct. The client renderer is completely unaware that templates exist. --- -## 6. Parameter Expressions & Substitution Engine +## 8. Parameter Expressions & Substitution Engine The template substitution engine replaces expressions according to strict typing and evaluation rules. @@ -518,7 +574,7 @@ To emit a literal `${foo}` string without triggering parameter substitution, esc --- -## 7. Deterministic Synthetic ID Generation Algorithm +## 9. Deterministic Synthetic ID Generation Algorithm To guarantee collision-free component IDs across multiple template instances and deep nesting, implementations in all languages (Python, Dart, TypeScript) must implement the following deterministic naming rules: @@ -555,7 +611,7 @@ This guarantees that two instances of the same template (`card_1` and `card_2`) --- -## 8. Higher-Order Container Templates (`child` and `children` Slots) +## 10. Higher-Order Container Templates (`child` and `children` Slots) Templates can define structural containers that receive caller-provided components. @@ -563,7 +619,9 @@ Templates can define structural containers that receive caller-provided componen ```yaml version: '0.1' -templateId: SectionCard +name: SectionCard +catalogs: + - 'https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json' parameters: title: {type: string} headerAction: {type: child} @@ -606,27 +664,30 @@ The template engine replaces `${headerAction}` with the synthesized single child --- -## 9. Error Handling, Cycle Guards & Safety +## 11. Error Handling, Cycle Guards & Safety ### Recursion & Circular Reference Detection Templates can invoke other templates. However, circular references (`A` invokes `B`, which invokes `A`) must be caught immediately to prevent stack overflow. - **Call Stack Tracking**: The `TemplateProcessor` maintains an active `_call_stack: Set[str]` throughout expansion. -- **Cycle Guard**: Before expanding any template, if `template_id in _call_stack`, expansion terminates immediately with `TemplateCycleError: Circular template reference detected: A -> B -> A`. +- **Cycle Guard**: Before expanding any template, if `template_name in _call_stack`, expansion terminates immediately with `TemplateCycleError: Circular template reference detected: A -> B -> A`. - **Maximum Depth Guard**: An absolute limit (`MAX_EXPANSION_DEPTH = 32`) enforces termination even in non-identical runaway recursion. ### Standard Error Hierarchy -1. `TemplateNotFoundError`: Attempted to expand a `templateId` not present in the catalog. +1. `TemplateNotFoundError`: Attempted to expand a template `name` or `id` not present in the registry. 2. `TemplateParameterError`: Missing required parameter or invalid parameter type. 3. `TemplateCycleError`: Circular reference detected during expansion. 4. `TemplateDepthExceededError`: Expansion exceeded maximum recursion depth. 5. `TemplateResolverError`: Exception raised by a dynamic template resolver function. +6. `CatalogCompatibilityError`: Declared catalogs incompatible with target surface (e.g. multi-catalog template on v0.9 surface). +7. `AmbiguousComponentError`: Component exists in multiple declared catalogs without disambiguation. +8. `UnknownComponentError`: Component not found in any declared catalog. --- -## 10. Multi-Document YAML Streams (`---`) +## 12. Multi-Document YAML Streams (`---`) To streamline template maintenance, multiple templates can be defined in a single `.yaml` file separated by `---`: @@ -635,7 +696,7 @@ To streamline template maintenance, multiple templates can be defined in a singl --- -## 11. Template Versioning & Schema Evolution +## 13. Template Versioning & Schema Evolution To ensure robust backward compatibility as template syntax and AST features evolve across future protocol releases, every template definition must declare a top-level `version` field. @@ -647,11 +708,15 @@ To ensure robust backward compatibility as template syntax and AST features evol --- -## 12. Conformance Checklist for SDK Implementations +## 14. Conformance Checklist for SDK Implementations An implementation of the A2UI Template Engine in any language (Python, Dart, TypeScript, Go, etc.) is conformant if and only if it satisfies the following test requirements: - [ ] **Template Versioning**: Validates that all ingested templates declare `version: "0.1"` and rejects unsupported version strings. +- [ ] **Catalog Decoupling**: Does not default to the Basic Catalog; requires catalogs to be passed explicitly and validates all components against them. +- [ ] **Catalog Resolution & Disambiguation**: Resolves components across declared catalogs without prefixes; handles collision disambiguation via component-level `catalogId`. +- [ ] **Protocol Version Output Rules**: Enforces v0.9 single-catalog surface constraints (stripping component `catalogId`); supports v1.0 mixable catalogs with automatic `catalogId` stamping. +- [ ] **Sub-Template Imports**: Supports Tier 1 (local by `name`) and Tier 2 (modular by `id` with `imports` list and alias dictionary). - [ ] **Multi-Document Ingestion**: Parses single-doc and multi-doc (`---`) YAML streams. - [ ] **Forward Reference Resolution**: Expands templates regardless of registration order. - [ ] **Strict Native Type Preservation**: Exact `${param}` substitutions preserve `int`, `float`, `bool`, `dict`, and `list` types. diff --git a/specification/proposals/templates/examples/basic_cards.yaml b/specification/proposals/templates/examples/basic_cards.yaml index d9baa44ea0..455c844506 100644 --- a/specification/proposals/templates/examples/basic_cards.yaml +++ b/specification/proposals/templates/examples/basic_cards.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: BasicUserProfile +name: BasicUserProfile +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Standard identity card for team members. parameters: userId: @@ -49,7 +51,9 @@ sampleData: --- version: "0.1" -templateId: BasicMetricCard +name: BasicMetricCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Metric display card supporting both literal values and dynamic data model bindings. parameters: title: diff --git a/specification/proposals/templates/examples/feedback_item.yaml b/specification/proposals/templates/examples/feedback_item.yaml index a5baad6a6e..a2f751cf93 100644 --- a/specification/proposals/templates/examples/feedback_item.yaml +++ b/specification/proposals/templates/examples/feedback_item.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: FeedbackItem +name: FeedbackItem +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Card showing feedback note, author, and rating. parameters: diff --git a/specification/proposals/templates/examples/goal_item.yaml b/specification/proposals/templates/examples/goal_item.yaml index 06b3d4d3ae..bb5e9f62a4 100644 --- a/specification/proposals/templates/examples/goal_item.yaml +++ b/specification/proposals/templates/examples/goal_item.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: GoalItem +name: GoalItem +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Card showing an individual objective with priority, title, target date, and action button. parameters: diff --git a/specification/proposals/templates/examples/nested_lists.yaml b/specification/proposals/templates/examples/nested_lists.yaml index 80d8a195a5..4cd4d14526 100644 --- a/specification/proposals/templates/examples/nested_lists.yaml +++ b/specification/proposals/templates/examples/nested_lists.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: NestedTeamRoster +name: NestedTeamRoster +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Team directory roster with styled header banner and nested team lists. parameters: directoryTitle: @@ -52,7 +54,9 @@ sampleData: --- version: "0.1" -templateId: NestedTeamCard +name: NestedTeamCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Team container card displaying member cards. parameters: teamName: diff --git a/specification/proposals/templates/examples/salary_card.yaml b/specification/proposals/templates/examples/salary_card.yaml index b1959b71b8..d141b2ef10 100644 --- a/specification/proposals/templates/examples/salary_card.yaml +++ b/specification/proposals/templates/examples/salary_card.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: SalaryCard +name: SalaryCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Layout card for employee compensation package with security verification badge. parameters: diff --git a/specification/proposals/templates/examples/section_card.yaml b/specification/proposals/templates/examples/section_card.yaml index 34ade454b4..c69a332c53 100644 --- a/specification/proposals/templates/examples/section_card.yaml +++ b/specification/proposals/templates/examples/section_card.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: SectionCard +name: SectionCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Standard container card with title, description, optional action, and child components. parameters: diff --git a/specification/proposals/templates/examples/slot_composition.yaml b/specification/proposals/templates/examples/slot_composition.yaml index c269f1992c..4d9dc10c70 100644 --- a/specification/proposals/templates/examples/slot_composition.yaml +++ b/specification/proposals/templates/examples/slot_composition.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: CompositeSectionCard +name: CompositeSectionCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Collapsible or styled content section card with title, custom header action slot, and children slot. parameters: title: @@ -55,7 +57,9 @@ sampleData: --- version: "0.1" -templateId: TwoColumnLayoutWithHeader +name: TwoColumnLayoutWithHeader +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Responsive two-column dashboard grid with a hero header, left column, and right column. parameters: headerChild: diff --git a/specification/proposals/templates/examples/team_card.yaml b/specification/proposals/templates/examples/team_card.yaml index d7f47ab03f..059e3134eb 100644 --- a/specification/proposals/templates/examples/team_card.yaml +++ b/specification/proposals/templates/examples/team_card.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TeamCard +name: TeamCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Card showing team header and unrolled member cards. parameters: diff --git a/specification/proposals/templates/examples/team_feedback_board.yaml b/specification/proposals/templates/examples/team_feedback_board.yaml index d7f874dac5..b78a12d059 100644 --- a/specification/proposals/templates/examples/team_feedback_board.yaml +++ b/specification/proposals/templates/examples/team_feedback_board.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TeamFeedbackBoard +name: TeamFeedbackBoard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Feedback board showing team header and feedback items. parameters: diff --git a/specification/proposals/templates/examples/team_goal_list.yaml b/specification/proposals/templates/examples/team_goal_list.yaml index 5a7c092c66..2ea5630d24 100644 --- a/specification/proposals/templates/examples/team_goal_list.yaml +++ b/specification/proposals/templates/examples/team_goal_list.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TeamGoalList +name: TeamGoalList +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: List of team goals with header banner. parameters: diff --git a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml index 4d68cd0258..1a99c354af 100644 --- a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml +++ b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TeamMemberKnowledgePanel +name: TeamMemberKnowledgePanel +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Card showing competency panel with user experience years and completed task count. parameters: diff --git a/specification/proposals/templates/examples/team_roster.yaml b/specification/proposals/templates/examples/team_roster.yaml index f0a96d0c0c..c7808427f1 100644 --- a/specification/proposals/templates/examples/team_roster.yaml +++ b/specification/proposals/templates/examples/team_roster.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TeamRoster +name: TeamRoster +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Team directory roster with styled header banner and nested team lists. parameters: diff --git a/specification/proposals/templates/examples/two_column_layout.yaml b/specification/proposals/templates/examples/two_column_layout.yaml index c2d1866885..38e23f8142 100644 --- a/specification/proposals/templates/examples/two_column_layout.yaml +++ b/specification/proposals/templates/examples/two_column_layout.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: TwoColumnLayout +name: TwoColumnLayout +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: Responsive two-column dashboard layout with banner header and split content regions. parameters: diff --git a/specification/proposals/templates/examples/user_profile.yaml b/specification/proposals/templates/examples/user_profile.yaml index 517215afa2..3f2a9d8e47 100644 --- a/specification/proposals/templates/examples/user_profile.yaml +++ b/specification/proposals/templates/examples/user_profile.yaml @@ -13,7 +13,9 @@ # limitations under the License. version: "0.1" -templateId: UserProfile +name: UserProfile +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" description: User profile card displaying avatar, full name, and role. parameters: diff --git a/specification/proposals/templates/schema/template_definition.json b/specification/proposals/templates/schema/template_definition.json index 653ad54c7d..0feb3f5886 100644 --- a/specification/proposals/templates/schema/template_definition.json +++ b/specification/proposals/templates/schema/template_definition.json @@ -3,17 +3,50 @@ "title": "A2UI Template Definition", "description": "Schema defining a parameterized A2UI declarative template authored in YAML with a nested layout tree and synthetic ID generation.", "type": "object", - "required": ["version", "templateId", "parameters"], + "required": ["version", "parameters", "catalogs"], "properties": { "version": { "type": "string", "const": "0.1", "description": "Specification version of the template definition format. Forced to '0.1'." }, + "id": { + "type": "string", + "description": "Optional globally unique identifier or URI for the template." + }, + "name": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_]*$", + "description": "Component tag name of the template used in layout trees and LLM prompts." + }, "templateId": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]*$", - "description": "Unique identifier and component tag name of the template." + "description": "Deprecated alias for 'name'." + }, + "catalogs": { + "description": "Catalog identifier (URI) or list of catalog identifiers against which components are resolved.", + "anyOf": [ + { + "type": "array", + "items": {"type": "string"}, + "minItems": 1 + }, + {"type": "string"} + ] + }, + "imports": { + "description": "Optional list or mapping of dependent templates imported by global ID.", + "anyOf": [ + { + "type": "array", + "items": {"type": "string"} + }, + { + "type": "object", + "additionalProperties": {"type": "string"} + } + ] }, "description": { "type": "string", @@ -47,7 +80,14 @@ "description": "Sample parameter values used for previewing and testing template inflation." } }, - "anyOf": [{"required": ["layout"]}, {"required": ["components"]}], + "allOf": [ + { + "anyOf": [{"required": ["name"]}, {"required": ["templateId"]}] + }, + { + "anyOf": [{"required": ["layout"]}, {"required": ["components"]}] + } + ], "additionalProperties": false, "$defs": { "ParameterDefinition": { @@ -220,6 +260,10 @@ "type": "string", "description": "Catalog component tag name." }, + "catalogId": { + "type": "string", + "description": "Optional catalog ID to disambiguate if the component name exists in multiple declared catalogs." + }, "child": { "anyOf": [{"$ref": "#/$defs/NestedTemplateNode"}, {"$ref": "#/$defs/TemplateValue"}] }, @@ -254,6 +298,10 @@ "component": { "type": "string", "description": "Catalog component tag name." + }, + "catalogId": { + "type": "string", + "description": "Optional catalog ID to disambiguate if the component name exists in multiple declared catalogs." } }, "additionalProperties": { From 65145a074a1f9198edc0d6436e3cde778c92531d Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:20:36 +0930 Subject: [PATCH 04/26] docs(templates): address review comments on diagrams, terminology, recursion, and max depth --- specification/proposals/templates/README.md | 106 +++++++++----------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/specification/proposals/templates/README.md b/specification/proposals/templates/README.md index e39b3984ac..cce48db944 100644 --- a/specification/proposals/templates/README.md +++ b/specification/proposals/templates/README.md @@ -1,8 +1,8 @@ -# A2UI Templates Specification +# A2UI Template Specification ## Abstract -This document defines the authoritative specification for the **A2UI Templates System**. +This document defines the authoritative specification for **A2UI Templates**. Templates provide a declarative, human-readable mechanism for authoring reusable UI subtrees in nested YAML or via native programmatic render functions. At generation time, an agent or Large Language Model (LLM) outputs compact, high-level template invocations (such as `UserProfile("u1", "Alice")` or `PayrollSummary("Eng", true)`). The backend SDK expands these invocations into standard, flat A2UI Basic Catalog components in a single synchronous pass before emitting standard A2UI protocol messages (`updateComponents`) to the client. @@ -18,57 +18,46 @@ This architecture achieves three fundamental objectives: The template engine operates entirely server-side within the A2UI Agent SDK: -``` -+-------------------------------------------------------------------------------+ -| 1. Authoring & Registration | -| - Static YAML files (with multi-doc '---' support & cross-references) | -| - Dynamic resolvers (data-binding mode & programmatic AST mode) | -+---------------------------------------+---------------------------------------+ - | - v -+-------------------------------------------------------------------------------+ -| 2. Synthetic Catalog Generation | -| - TemplateProcessor scans registered templates | -| - Synthesizes virtual A2UI Component Catalog JSON schema | -+---------------------------------------+---------------------------------------+ - | - v -+-------------------------------------------------------------------------------+ -| 3. Model Prompt Generation & Inference | -| - PromptGenerator injects template signatures into system prompt | -| - LLM emits compact Express DSL / JSON: | -| root = TeamRoster("Engineering", [TeamCard("Core", [...])]) | -+---------------------------------------+---------------------------------------+ - | - v -+-------------------------------------------------------------------------------+ -| 4. Synchronous Template Expansion (TemplateProcessor) | -| - Resolves dynamic callbacks or executes programmatic render functions | -| - Substitutes typed parameters & evaluates dot-notation paths | -| - Unrolls server loops & plumbs component slot arguments | -| - Assigns deterministic synthetic IDs: {parent}_{slot}_{index}_{type} | -+---------------------------------------+---------------------------------------+ - | - v -+-------------------------------------------------------------------------------+ -| 5. Standard A2UI Protocol Emission | -| - Emits standard createSurface and updateComponents messages | -| - Payload contains 100% standard Basic Catalog primitives | -+---------------------------------------+---------------------------------------+ - | - v -+-------------------------------------------------------------------------------+ -| 6. Client Renderer Execution | -| - Standard Basic Catalog renderers paint the flat component graph | -| - Binds reactive two-way paths to client DataModel | -+-------------------------------------------------------------------------------+ +```mermaid +flowchart TD + subgraph Stage1["1. Authoring & Registration"] + YAML["Static YAML Files
(multi-doc '---' & cross-references)"] + Dynamic["Dynamic Resolvers
(data-binding & programmatic AST)"] + end + + subgraph Stage2["2. Synthetic Catalog Generation"] + Proc["TemplateProcessor indexes templates
& synthesizes virtual Component Catalog"] + end + + subgraph Stage3["3. Model Prompt Generation & Inference"] + Prompt["PromptGenerator injects template signatures into prompt"] + LLM["LLM emits compact Express DSL / JSON
(e.g. root = TeamRoster(...))"] + end + + subgraph Stage4["4. Synchronous Template Expansion"] + Expand["TemplateProcessor unrolls loops,
substitutes params, assigns synthetic IDs"] + end + + subgraph Stage5["5. Standard A2UI Protocol Emission"] + Msg["Emits createSurface & updateComponents
(100% standard Basic Catalog primitives)"] + end + + subgraph Stage6["6. Client Renderer Execution"] + Client["Client renderers paint standard components
& bind reactive paths to client DataModel"] + end + + Stage1 --> Stage2 + Stage2 --> Stage3 + Stage3 --> Stage4 + Stage4 --> Stage5 + Stage5 --> Stage6 ``` --- ## 2. Template Taxonomy -A2UI defines three template modalities: +A2UI supports three template authoring styles: ### A. Declarative Static Templates (YAML) @@ -478,13 +467,11 @@ When `TemplateProcessor(templates, catalogs)` is initialized: ### Surface Isolation Boundary -``` -[LLM Agent] <---> [Synthetic Catalog (Templates + Primitives)] <---> [TemplateProcessor] - | - (Expands to Primitives) - | - v -[Client Renderer] <---------------- (Standard Catalog Components) <---------+ +```mermaid +flowchart LR + Agent["LLM Agent"] <--> SyntheticCatalog["Synthetic Catalog
(Templates + Primitives)"] + SyntheticCatalog <--> Processor["TemplateProcessor"] + Processor -->|"Synchronously expands
to flat primitives"| Client["Client Renderer
(Basic Catalog)"] ``` The synthetic catalog is an ephemeral compile-time construct. The client renderer is completely unaware that templates exist. @@ -668,11 +655,14 @@ The template engine replaces `${headerAction}` with the synthesized single child ### Recursion & Circular Reference Detection -Templates can invoke other templates. However, circular references (`A` invokes `B`, which invokes `A`) must be caught immediately to prevent stack overflow. +Templates can invoke other templates. However, circular references in template definitions (`A` invokes `B`, which invokes `A`) must be caught immediately to prevent stack overflow. -- **Call Stack Tracking**: The `TemplateProcessor` maintains an active `_call_stack: Set[str]` throughout expansion. -- **Cycle Guard**: Before expanding any template, if `template_name in _call_stack`, expansion terminates immediately with `TemplateCycleError: Circular template reference detected: A -> B -> A`. -- **Maximum Depth Guard**: An absolute limit (`MAX_EXPANSION_DEPTH = 32`) enforces termination even in non-identical runaway recursion. +- **Definition Cycles vs. Instance Nesting**: + - **Definition Cycles (Prohibited)**: The cycle guard specifically tracks the chain of template _definitions_ being expanded (`TemplateA -> TemplateB -> TemplateA`). If a template definition unconditionally references itself or another template in a cycle, expansion terminates with `TemplateCycleError`. + - **Instance Nesting (Fully Supported)**: When a caller passes an instance of a template as an argument into another template's `child` or `children` slot (for example, a `Card` template containing a `Carousel` template whose items are other `Card` template instances), this is standard, safe hierarchical composition. Each instance receives a unique synthetic ID and is bounded by caller input. +- **Call Stack Tracking**: The `TemplateProcessor` maintains an active `_call_stack: Set[str]` of template definition names currently being evaluated. +- **Cycle Guard**: Before expanding any template definition, if `template_name in _call_stack`, expansion terminates immediately with `TemplateCycleError: Circular template reference detected: A -> B -> A`. +- **Maximum Depth Guard**: An absolute limit (`MAX_EXPANSION_DEPTH = 50`) enforces termination even in deeply nested or runaway recursion. ### Standard Error Hierarchy From 650a511b13fda1b19a7c7ac2df14649caaf58967 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:26:50 +0930 Subject: [PATCH 05/26] feat(spec): adopt double-curly Mustache syntax for template parameter substitutions --- specification/proposals/templates/README.md | 82 +++++++++++-------- .../templates/examples/basic_cards.yaml | 8 +- .../templates/examples/feedback_item.yaml | 6 +- .../templates/examples/goal_item.yaml | 6 +- .../templates/examples/nested_lists.yaml | 8 +- .../templates/examples/salary_card.yaml | 14 ++-- .../templates/examples/section_card.yaml | 8 +- .../templates/examples/slot_composition.yaml | 12 +-- .../templates/examples/team_card.yaml | 2 +- .../examples/team_feedback_board.yaml | 2 +- .../templates/examples/team_goal_list.yaml | 2 +- .../examples/team_member_knowledge_panel.yaml | 8 +- .../templates/examples/team_roster.yaml | 4 +- .../templates/examples/two_column_layout.yaml | 6 +- .../templates/examples/user_profile.yaml | 4 +- 15 files changed, 95 insertions(+), 77 deletions(-) diff --git a/specification/proposals/templates/README.md b/specification/proposals/templates/README.md index cce48db944..fd2ec6b2a1 100644 --- a/specification/proposals/templates/README.md +++ b/specification/proposals/templates/README.md @@ -89,10 +89,10 @@ layout: - component: Icon name: person - component: Text - text: ${userName} + text: '{{ userName }}' variant: h3 - component: Text - text: ${role} + text: '{{ role }}' variant: caption ``` @@ -199,7 +199,7 @@ layout: component: Column children: - component: Text - text: 'Objectives: ${teamName}' + text: 'Objectives: {{ teamName }}' variant: h2 - component: Column children: @@ -211,9 +211,9 @@ layout: justify: spaceBetween children: - component: Text - text: ${goal.title} + text: '{{ goal.title }}' - component: Text - text: ${goal.priority} + text: '{{ goal.priority }}' variant: caption ``` @@ -324,10 +324,10 @@ layout: component: Column children: - component: Text - text: ${label} + text: '{{ label }}' - component: Text text: - path: '/system/metrics/${metricKey}/currentValue' + path: '/system/metrics/{{ metricKey }}/currentValue' ``` **Expanded Output:** @@ -366,13 +366,14 @@ layout: component: Column children: - component: Text - text: ${title} + text: '{{ title }}' - component: Text - text: ${val} + text: '{{ val }}' ``` **Expanded Output:** -Because `${val}` is an exact-match substitution, the dictionary type is strictly preserved: + +Because `{{ val }}` is an exact-match substitution, the dictionary type is strictly preserved: ```json { @@ -480,25 +481,42 @@ The synthetic catalog is an ephemeral compile-time construct. The client rendere ## 8. Parameter Expressions & Substitution Engine -The template substitution engine replaces expressions according to strict typing and evaluation rules. +The template substitution engine replaces expressions according to strict typing and evaluation rules using standard double-curly Mustache syntax (`{{ param }}`). ### Rule 1: Exact Match Substitution (Type Preserving) -If a string field value exactly matches the parameter token regex `^\$\{([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\}$`: +If a string field value exactly matches the parameter token regex `^\{\{\s*([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\s*\}\}$`: - The entire field is replaced by the native runtime value of the parameter. -- **Integers remain integers**: `level: ${userLevel}` $\rightarrow$ `level: 4` (not `"4"`). -- **Booleans remain booleans**: `checked: ${isActive}` $\rightarrow$ `checked: true` (not `"true"`). -- **Objects and Arrays remain native structures**: `items: ${memberList}` $\rightarrow$ `items: [{...}, {...}]`. +- **Integers remain integers**: `level: "{{ userLevel }}"` $\rightarrow$ `level: 4` (not `"4"`). +- **Booleans remain booleans**: `checked: "{{ isActive }}"` $\rightarrow$ `checked: true` (not `"true"`). +- **Objects and Arrays remain native structures**: `items: "{{ memberList }}"` $\rightarrow$ `items: [{...}, {...}]`. ### Rule 2: Embedded String Interpolation -If a parameter token appears alongside other characters (e.g. `"Hello, ${userName}!"` or `"${dept} - ${code}"`): +If a parameter token appears alongside other characters (e.g. `"Hello, {{ userName }}!"` or `"{{ dept }} - {{ code }}"`): -- All tokens are replaced by their string representations. +- All tokens matching `\{\{\s*([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\s*\}\}` are replaced by their string representations. - Evaluates to a native string. -### Rule 3: Deep Dot-Notation for Structured Objects +### Rule 3: Client DataModel Disambiguation (`formatString`) + +Because template substitutions use double curlies `{{ ... }}`, client-side reactive expressions and data-model bindings in `formatString` (e.g. `${/user/name}`) pass through completely untouched without requiring escaping: + +```yaml +component: Text +text: + call: formatString + args: + value: 'Hello, ${/user/name}! Your team is {{ teamName }}.' +``` + +During server-side template expansion: + +- `{{ teamName }}` is resolved server-side and replaced with `"Engineering"`. +- `${/user/name}` is preserved verbatim and emitted directly to the client renderer for reactive data-binding. + +### Rule 4: Deep Dot-Notation for Structured Objects Templates natively support deep dot-notation paths on object parameters: @@ -524,7 +542,7 @@ layout: component: Card child: component: Text - text: '${account.profile.name} (${account.profile.tier})' + text: '{{ account.profile.name }} ({{ account.profile.tier }})' ``` **Expanded Output:** @@ -536,24 +554,24 @@ layout: } ``` -### Rule 4: String Format AST Expressions +### Rule 5: String Format AST Expressions For complex string formatting across multiple languages: ```yaml format: 'Level {lvl} ({pts} points)' args: - lvl: ${level} - pts: ${experiencePoints} + lvl: '{{ level }}' + pts: '{{ experiencePoints }}' ``` -### Rule 5: Token Escaping +### Rule 6: Token Escaping -To emit a literal `${foo}` string without triggering parameter substitution, escape the leading dollar sign with a backslash: +To emit a literal `{{ foo }}` string without triggering parameter substitution, escape the opening curly braces with a backslash: -- `\${keep_literal}` $\rightarrow$ evaluates to literal `"${keep_literal}"`. +- `\{{ keep_literal }}` $\rightarrow$ evaluates to literal `"{{ keep_literal }}"`. -### Rule 6: Missing Values & Defaults +### Rule 7: Missing Values & Defaults - If an argument is omitted but has a declared `default` in its parameter schema, the `default` is used. - If an argument is omitted, has no `default`, and is not required: the property is omitted from the synthesized component (it is not emitted as `null`). @@ -624,14 +642,14 @@ layout: align: center children: - component: Text - text: ${title} + text: '{{ title }}' variant: h2 - component: Column - children: ${headerAction} + children: '{{ headerAction }}' - component: Divider axis: horizontal - component: Column - children: ${children} + children: '{{ children }}' ``` #### Model Invocation: @@ -647,7 +665,7 @@ root = SectionCard( ) ``` -The template engine replaces `${headerAction}` with the synthesized single child node, and concatenates the `${children}` component list into the body column. +The template engine replaces `{{ headerAction }}` with the synthesized single child node, and concatenates the `{{ children }}` component list into the body column. --- @@ -709,8 +727,8 @@ An implementation of the A2UI Template Engine in any language (Python, Dart, Typ - [ ] **Sub-Template Imports**: Supports Tier 1 (local by `name`) and Tier 2 (modular by `id` with `imports` list and alias dictionary). - [ ] **Multi-Document Ingestion**: Parses single-doc and multi-doc (`---`) YAML streams. - [ ] **Forward Reference Resolution**: Expands templates regardless of registration order. -- [ ] **Strict Native Type Preservation**: Exact `${param}` substitutions preserve `int`, `float`, `bool`, `dict`, and `list` types. -- [ ] **Dot-Notation Path Traversal**: Resolves arbitrary depth paths (e.g. `${user.details.address.zip}`). +- [ ] **Strict Native Type Preservation**: Exact `{{ param }}` substitutions preserve `int`, `float`, `bool`, `dict`, and `list` types. +- [ ] **Dot-Notation Path Traversal**: Resolves arbitrary depth paths (e.g. `{{ user.details.address.zip }}`). - [ ] **Inline and Named Loop Expansion**: Accurately unrolls arrays into synthesized component subtrees with indexed child IDs. - [ ] **Deterministic Synthetic IDs**: Emits canonical hierarchical IDs matching the `{parent}_{slot}_{index}_{type}` specification. - [ ] **Two-Way DataModel Passthrough**: Preserves `{path: "..."}` dictionary bindings without string conversion. diff --git a/specification/proposals/templates/examples/basic_cards.yaml b/specification/proposals/templates/examples/basic_cards.yaml index 455c844506..9b03872d3a 100644 --- a/specification/proposals/templates/examples/basic_cards.yaml +++ b/specification/proposals/templates/examples/basic_cards.yaml @@ -38,10 +38,10 @@ layout: - component: Icon name: person - component: Text - text: ${userName} + text: "{{ userName }}" variant: h3 - component: Text - text: ${role} + text: "{{ role }}" variant: caption sampleData: @@ -73,10 +73,10 @@ layout: component: Column children: - component: Text - text: ${title} + text: "{{ title }}" variant: caption - component: Text - text: ${metricValue} + text: "{{ metricValue }}" variant: h1 sampleData: diff --git a/specification/proposals/templates/examples/feedback_item.yaml b/specification/proposals/templates/examples/feedback_item.yaml index a2f751cf93..74796a58eb 100644 --- a/specification/proposals/templates/examples/feedback_item.yaml +++ b/specification/proposals/templates/examples/feedback_item.yaml @@ -41,7 +41,7 @@ layout: component: Column children: - component: Text - text: ${note} + text: "{{ note }}" variant: body - component: Divider axis: horizontal @@ -50,10 +50,10 @@ layout: align: center children: - component: Text - text: ${author} + text: "{{ author }}" variant: caption - component: Text - text: "Rating: ${rating}/5" + text: "Rating: {{ rating }}/5" variant: caption sampleData: diff --git a/specification/proposals/templates/examples/goal_item.yaml b/specification/proposals/templates/examples/goal_item.yaml index bb5e9f62a4..f1be35d83f 100644 --- a/specification/proposals/templates/examples/goal_item.yaml +++ b/specification/proposals/templates/examples/goal_item.yaml @@ -48,12 +48,12 @@ layout: align: center children: - component: Text - text: "Priority: ${priority}" + text: "Priority: {{ priority }}" variant: caption - component: Icon name: star - component: Text - text: ${title} + text: "{{ title }}" variant: h4 - component: Divider axis: horizontal @@ -62,7 +62,7 @@ layout: align: center children: - component: Text - text: "Due: ${targetDate}" + text: "Due: {{ targetDate }}" variant: caption - component: Button child: diff --git a/specification/proposals/templates/examples/nested_lists.yaml b/specification/proposals/templates/examples/nested_lists.yaml index 4cd4d14526..8736a150b8 100644 --- a/specification/proposals/templates/examples/nested_lists.yaml +++ b/specification/proposals/templates/examples/nested_lists.yaml @@ -30,7 +30,7 @@ layout: component: Column children: - component: Text - text: ${directoryTitle} + text: "{{ directoryTitle }}" variant: h1 - component: Divider axis: horizontal @@ -73,7 +73,7 @@ layout: component: Column children: - component: Text - text: ${teamName} + text: "{{ teamName }}" variant: h2 - component: Divider axis: horizontal @@ -87,9 +87,9 @@ layout: justify: spaceBetween children: - component: Text - text: ${member.userName} + text: "{{ member.userName }}" - component: Text - text: ${member.role} + text: "{{ member.role }}" variant: caption sampleData: diff --git a/specification/proposals/templates/examples/salary_card.yaml b/specification/proposals/templates/examples/salary_card.yaml index d141b2ef10..167dc967b4 100644 --- a/specification/proposals/templates/examples/salary_card.yaml +++ b/specification/proposals/templates/examples/salary_card.yaml @@ -68,16 +68,16 @@ layout: text: "Verified Compensation" variant: caption - component: Text - text: ${clearanceLevel} + text: "{{ clearanceLevel }}" variant: caption - component: Column children: - component: Text - text: ${employeeName} + text: "{{ employeeName }}" variant: h3 id: name_txt - component: Text - text: ${role} + text: "{{ role }}" variant: body - component: Divider axis: horizontal @@ -90,7 +90,7 @@ layout: text: "Base Salary" variant: caption - component: Text - text: ${baseSalary} + text: "{{ baseSalary }}" variant: h4 id: sal_val - component: Column @@ -99,7 +99,7 @@ layout: text: "Annual Bonus" variant: caption - component: Text - text: ${annualBonus} + text: "{{ annualBonus }}" variant: h4 - component: Column children: @@ -107,7 +107,7 @@ layout: text: "Equity Grants" variant: caption - component: Text - text: ${equity} + text: "{{ equity }}" variant: h4 - component: Divider axis: horizontal @@ -119,7 +119,7 @@ layout: text: "🔒 Fetched live from secure HR database" variant: caption - component: Text - text: "Verified: ${verifiedAt}" + text: "Verified: {{ verifiedAt }}" variant: caption sampleData: diff --git a/specification/proposals/templates/examples/section_card.yaml b/specification/proposals/templates/examples/section_card.yaml index c69a332c53..b64d9aad4f 100644 --- a/specification/proposals/templates/examples/section_card.yaml +++ b/specification/proposals/templates/examples/section_card.yaml @@ -48,19 +48,19 @@ layout: align: center children: - component: Text - text: ${title} + text: "{{ title }}" variant: h3 - component: Column id: action_slot - children: ${headerAction} + children: "{{ headerAction }}" - component: Text - text: ${description} + text: "{{ description }}" variant: caption - component: Divider axis: horizontal - component: Column id: body_container - children: ${children} + children: "{{ children }}" sampleData: title: Protocol Overview diff --git a/specification/proposals/templates/examples/slot_composition.yaml b/specification/proposals/templates/examples/slot_composition.yaml index 4d9dc10c70..51cba60a03 100644 --- a/specification/proposals/templates/examples/slot_composition.yaml +++ b/specification/proposals/templates/examples/slot_composition.yaml @@ -39,14 +39,14 @@ layout: align: center children: - component: Text - text: ${title} + text: "{{ title }}" variant: h2 - component: Column - children: ${headerAction} + children: "{{ headerAction }}" - component: Divider axis: horizontal - component: Column - children: ${children} + children: "{{ children }}" sampleData: title: Project Milestones @@ -76,7 +76,7 @@ layout: component: Column children: - component: Column - children: ${headerChild} + children: "{{ headerChild }}" - component: Divider axis: horizontal - component: Row @@ -84,9 +84,9 @@ layout: align: start children: - component: Column - children: ${leftChildren} + children: "{{ leftChildren }}" - component: Column - children: ${rightChildren} + children: "{{ rightChildren }}" sampleData: headerChild: diff --git a/specification/proposals/templates/examples/team_card.yaml b/specification/proposals/templates/examples/team_card.yaml index 059e3134eb..d1ec4d163b 100644 --- a/specification/proposals/templates/examples/team_card.yaml +++ b/specification/proposals/templates/examples/team_card.yaml @@ -58,7 +58,7 @@ layout: align: center children: - component: Text - text: ${teamName} + text: "{{ teamName }}" variant: h3 - component: Icon name: person diff --git a/specification/proposals/templates/examples/team_feedback_board.yaml b/specification/proposals/templates/examples/team_feedback_board.yaml index b78a12d059..544f0dfcaa 100644 --- a/specification/proposals/templates/examples/team_feedback_board.yaml +++ b/specification/proposals/templates/examples/team_feedback_board.yaml @@ -60,7 +60,7 @@ layout: - component: Icon name: mail - component: Text - text: "Feedback & Retrospective: ${teamName}" + text: "Feedback & Retrospective: {{ teamName }}" variant: h2 - component: Column id: feedbacks_container diff --git a/specification/proposals/templates/examples/team_goal_list.yaml b/specification/proposals/templates/examples/team_goal_list.yaml index 2ea5630d24..db6241144a 100644 --- a/specification/proposals/templates/examples/team_goal_list.yaml +++ b/specification/proposals/templates/examples/team_goal_list.yaml @@ -61,7 +61,7 @@ layout: - component: Icon name: star - component: Text - text: "Strategic Objectives: ${teamName}" + text: "Strategic Objectives: {{ teamName }}" variant: h2 - component: Column id: goals_container diff --git a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml index 1a99c354af..fad8d61bdf 100644 --- a/specification/proposals/templates/examples/team_member_knowledge_panel.yaml +++ b/specification/proposals/templates/examples/team_member_knowledge_panel.yaml @@ -49,7 +49,7 @@ layout: - component: Icon name: check - component: Text - text: "Competency: ${userName}" + text: "Competency: {{ userName }}" variant: h4 - component: Divider axis: horizontal @@ -63,7 +63,7 @@ layout: text: Role variant: caption - component: Text - text: ${role} + text: "{{ role }}" variant: body - component: Column align: center @@ -72,7 +72,7 @@ layout: text: Experience variant: caption - component: Text - text: "${experienceYears} Yrs" + text: "{{ experienceYears }} Yrs" variant: body - component: Column align: center @@ -81,7 +81,7 @@ layout: text: Tasks variant: caption - component: Text - text: "${completedTasks} Done" + text: "{{ completedTasks }} Done" variant: body - component: Text text: Verified Core Contributor diff --git a/specification/proposals/templates/examples/team_roster.yaml b/specification/proposals/templates/examples/team_roster.yaml index c7808427f1..b0261c85f4 100644 --- a/specification/proposals/templates/examples/team_roster.yaml +++ b/specification/proposals/templates/examples/team_roster.yaml @@ -33,12 +33,12 @@ layout: component: Column children: - component: Text - text: ${directoryTitle} + text: "{{ directoryTitle }}" variant: h1 - component: Divider axis: horizontal - component: Column - children: ${children} + children: "{{ children }}" sampleData: directoryTitle: Global Engineering Directory diff --git a/specification/proposals/templates/examples/two_column_layout.yaml b/specification/proposals/templates/examples/two_column_layout.yaml index 38e23f8142..41c4ee2585 100644 --- a/specification/proposals/templates/examples/two_column_layout.yaml +++ b/specification/proposals/templates/examples/two_column_layout.yaml @@ -38,15 +38,15 @@ layout: component: Column children: - component: Column - children: ${headerChild} + children: "{{ headerChild }}" - component: Divider axis: horizontal - component: Row children: - component: Column - children: ${leftChildren} + children: "{{ leftChildren }}" - component: Column - children: ${rightChildren} + children: "{{ rightChildren }}" sampleData: headerChild: diff --git a/specification/proposals/templates/examples/user_profile.yaml b/specification/proposals/templates/examples/user_profile.yaml index 3f2a9d8e47..f73bb276dd 100644 --- a/specification/proposals/templates/examples/user_profile.yaml +++ b/specification/proposals/templates/examples/user_profile.yaml @@ -42,10 +42,10 @@ layout: - component: Icon name: person - component: Text - text: ${userName} + text: "{{ userName }}" variant: h3 - component: Text - text: ${role} + text: "{{ role }}" variant: caption sampleData: From ce458060330895ac683035079a65d76c8238036a Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 13:12:35 +0930 Subject: [PATCH 06/26] feat(python): implement A2UI synchronous template expansion engine with version 0.1 validation --- agent_sdks/python/a2ui_agent/README.md | 7 + agent_sdks/python/a2ui_agent/pyproject.toml | 5 +- .../experimental/template/README.md | 118 +++ .../experimental/template/__init__.py | 79 ++ .../experimental/template/format.py | 257 +++++ .../experimental}/template/manager.py | 0 .../experimental/template/models.py | 869 ++++++++++++++++ .../experimental/template/processor.py | 917 +++++++++++++++++ .../template/schema/template_definition.json | 273 +++++ .../a2ui_agent/src/a2ui/template/__init__.py | 13 - .../template/test_template_format.py | 952 ++++++++++++++++++ uv.lock | 15 + 12 files changed, 3491 insertions(+), 14 deletions(-) create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py rename agent_sdks/python/a2ui_agent/src/a2ui/{ => inference_formats/experimental}/template/manager.py (100%) create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py create mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json delete mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/template/__init__.py create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py diff --git a/agent_sdks/python/a2ui_agent/README.md b/agent_sdks/python/a2ui_agent/README.md index e18deb7db0..67dd34caaf 100644 --- a/agent_sdks/python/a2ui_agent/README.md +++ b/agent_sdks/python/a2ui_agent/README.md @@ -28,6 +28,13 @@ The following directories contain the base protocol logic, parsing, and schema o - **`provider.py`**: Implementation of `BasicCatalog` for handling the basic A2UI components. +## Experimental Templates (`src/a2ui/inference_formats/experimental/template`) + +- **`models.py`**: Typed models (`StaticTemplate`, `DynamicTemplate`, `Param`, `ParamType`, AST expressions) and signature-based parameter inference. +- **`processor.py`**: `TemplateProcessor` for generating synthetic inference catalogs and expanding template invocations into A2UI primitives. +- **`format.py`**: `TemplateInferenceFormat` integrating template-aware prompt generation and synchronous parsing. +- **`schema/template_definition.json`**: Formal JSON Schema for template structure and parameter types. + ## A2A (`src/a2ui/a2a`) - **`extension.py`**: Utilities for managing the A2UI extension URI and activation logic. diff --git a/agent_sdks/python/a2ui_agent/pyproject.toml b/agent_sdks/python/a2ui_agent/pyproject.toml index 0ef8ab3358..ef4e4d572b 100644 --- a/agent_sdks/python/a2ui_agent/pyproject.toml +++ b/agent_sdks/python/a2ui_agent/pyproject.toml @@ -25,7 +25,9 @@ dependencies = [ "google-adk>=1.28.1", "google-genai>=1.27.0", "httpx>=0.27.0", - "jsonschema>=4.0.0" + "jsonschema>=4.0.0", + "nest-asyncio>=1.6.0", + "pyyaml>=6.0" ] [project.scripts] @@ -65,4 +67,5 @@ dev = [ "antlr4-tools>=0.2.1", "hatchling>=1.30.1", "types-jsonschema", + "types-pyyaml", ] diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md new file mode 100644 index 0000000000..303f4dece0 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md @@ -0,0 +1,118 @@ +# A2UI Python Template Engine (`a2ui.inference_formats.experimental.template`) + +The `a2ui.inference_formats.experimental.template` package provides server-side UI template expansion and synthetic catalog compilation for the A2UI Python Agent SDK. + +For the language-agnostic protocol specification, JSON schemas, and multi-platform design guidelines, see: +👉 **[`specification/proposals/templates/README.md`](../../../../../../specification/proposals/templates/README.md)** + +--- + +## Python API Reference + +### 1. `StaticTemplate` +Represents a declarative, immutable UI template parsed from YAML or a dictionary. + +```python +from a2ui.inference_formats.experimental.template import StaticTemplate + +# Load from single or multi-document ('---') YAML files +templates = StaticTemplate.from_yaml_file("path/to/templates.yaml") + +# Load directly from a YAML string +single_template = StaticTemplate.from_yaml(yaml_string) +``` + +### 2. `DynamicTemplate` +Extends templates with server-side execution. Supports two modes: + +#### A. Data-Binding Mode (`resolver + layout`) +Takes a lookup argument (e.g. `employeeId`), runs a resolver callback, and injects the result into a static YAML presentation layout: + +```python +from a2ui.inference_formats.experimental.template import DynamicTemplate, StaticTemplate + +layout = StaticTemplate.from_yaml_file("salary_card.yaml")[0] + +def resolve_compensation(employeeId: str) -> dict: + return db.fetch_employee_salary(employeeId) + +dynamic_salary = DynamicTemplate( + template_id="EmployeeSalaryCard", + resolver=resolve_compensation, + layout=layout, + description="Verified compensation. Pass only employeeId.", + sample_data={"employeeId": "emp_101"}, +) +``` + +#### B. Programmatic Render Mode (`render`) +Bypasses static layouts and runs native Python logic (loops, arithmetic, conditionals) returning a component AST: + +```python +from a2ui.inference_formats.experimental.template import DynamicTemplate + +def render_server_health(serverId: str, includeDisks: bool = True) -> dict: + status_icon = "check_circle" if serverId == "srv_01" else "warning" + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + {"component": "Text", "text": f"Server {serverId}", "variant": "h3"}, + {"component": "Icon", "name": status_icon}, + ] + } + } + +server_template = DynamicTemplate( + template_id="ServerHealthCard", + render=render_server_health, + description="Live server diagnostics.", + sample_data={"serverId": "srv_01", "includeDisks": True}, +) +``` + +### 3. `TemplateProcessor` +The core synchronous template expansion and catalog generation engine. + +```python +from a2ui.inference_formats.experimental.template import ( + TemplateProcessor, + TemplateId, + A2UIComponentList, +) + +processor = TemplateProcessor(templates=[user_profile, dynamic_salary, server_template]) + +# 1. Synthesize virtual A2UI component catalog for LLMs +synthetic_catalog = processor.generate_inference_catalog() + +# 2. Synchronously expand template invocations into standard A2UI primitives +expanded_components: A2UIComponentList = processor.expand_template( + instance_id="root", + template_id="ServerHealthCard", + passed_params={"serverId": "srv_01", "includeDisks": True}, +) +``` + +### 4. `TemplateInferenceFormat` +Integrates templates seamlessly with LLM prompting and parsing. + +```python +from a2ui.inference_formats.experimental.template import TemplateInferenceFormat + +format_instance = TemplateInferenceFormat( + templates=[user_profile, dynamic_salary], + surface_id="main", + version="0.9.1", +) + +# Generate system prompt instructions with template signatures +system_prompt = format_instance.prompt_generator.generate( + role_description="You are an A2UI assistant.", + include_schema=True, +) + +# Parse response & synchronously expand templates to standard A2UI messages +messages = format_instance.parser.parse_response(llm_response_text) +``` diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py new file mode 100644 index 0000000000..fb917bb24f --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py @@ -0,0 +1,79 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A2UI Template module providing parameterized component expansion and dynamic resolvers.""" + +from .models import ( + Template, + StaticTemplate, + DynamicTemplate, + BaseTemplate, + Param, + ParamType, + ParamRef, + Concat, + FormatExpr, + TemplateLoop, + TemplateComponent, + normalize_a2ui_type_to_jsonschema, + flatten_nested_layout, + normalize_node, +) +from .processor import ( + TemplateProcessor, + TemplateId, + InstanceId, + ComponentId, + ParamPath, + TemplateParams, + TemplateNodeDict, + A2UIComponent, + A2UIComponentList, + A2UIMessage, + CatalogSchema, + JSONSchemaDict, +) +from .format import TemplateInferenceFormat, A2uiTemplateManager, TemplateParser + +__all__ = [ + "Template", + "StaticTemplate", + "DynamicTemplate", + "BaseTemplate", + "Param", + "ParamType", + "ParamRef", + "Concat", + "FormatExpr", + "TemplateLoop", + "TemplateComponent", + "normalize_a2ui_type_to_jsonschema", + "flatten_nested_layout", + "normalize_node", + "TemplateProcessor", + "TemplateId", + "InstanceId", + "ComponentId", + "ParamPath", + "TemplateParams", + "TemplateNodeDict", + "A2UIComponent", + "A2UIComponentList", + "A2UIMessage", + "CatalogSchema", + "JSONSchemaDict", + "TemplateInferenceFormat", + "A2uiTemplateManager", + "TemplateParser", +] diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py new file mode 100644 index 0000000000..725d217001 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py @@ -0,0 +1,257 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Inference format coordinating prompt generation and parsing of LLM responses using templates.""" + +from __future__ import annotations + +from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from a2ui.core.catalog import Catalog +from a2ui.inference_format import InferenceFormat +from a2ui.parser.parser import Parser +from a2ui.parser.response_part import ResponsePart +from a2ui.prompt import PromptGenerator +from a2ui.schema.catalog import A2uiCatalog +from a2ui.schema.constants import ( + COMMON_TYPES_SCHEMA_KEY, + SERVER_TO_CLIENT_SCHEMA_KEY, + SPEC_VERSION_MAP, +) +from a2ui.schema.utils import load_from_bundled_resource +from google.adk.utils.feature_decorator import experimental + +from .models import Template, StaticTemplate, DynamicTemplate, BaseTemplate +from .processor import TemplateProcessor + + +def _clean_version(version: str) -> str: + """Normalizes version string by removing leading 'v' if present.""" + return version.lstrip("v") + + +class TemplateParser(Parser): + """Parser decorator that runs template expansion on compiled A2UI messages.""" + + def __init__(self, underlying_parser: Parser, processor: TemplateProcessor): + """Initializes the TemplateParser. + + Args: + underlying_parser: The underlying inference format parser (e.g. + ExpressParser). + processor: The TemplateProcessor instance to expand templates. + """ + self.underlying_parser = underlying_parser + self.processor = processor + + def has_format_content(self, content: str, *, complete: bool = False) -> bool: + """Checks if the content contains blocks belonging to the underlying parser.""" + return self.underlying_parser.has_format_content(content, complete=complete) + + def unwrap(self, content: str) -> List[ResponsePart]: + """Tokenizes response content into raw format-content parts.""" + return self.underlying_parser.unwrap(content) + + def compile( + self, format_content: str, *, is_final: bool = True + ) -> List[Dict[str, Any]]: + """Compiles raw format content and expands all template components.""" + raw_msgs = self.underlying_parser.compile(format_content, is_final=is_final) + expanded = self.processor.process_message(raw_msgs) + if isinstance(expanded, list): + return expanded + elif isinstance(expanded, dict): + return [expanded] + return [] + + def parse_response(self, content: str) -> List[ResponsePart]: + """Parses full response content into standard JSON payload parts by unwrapping and expanding templates.""" + parts = self.unwrap(content) + parsed_so_far: List[ResponsePart] = [] + for part in parts: + if part.a2ui_raw is not None: + try: + part.a2ui_json = self.compile(part.a2ui_raw, is_final=part.is_final) + except Exception as e: + from a2ui.parser.errors import A2uiCompilationError + + if isinstance(e, A2uiCompilationError): + e.partial_results = parsed_so_far + raise e + raise A2uiCompilationError( + message=str(e), + raw_content=part.a2ui_raw, + partial_results=parsed_so_far, + ) from e + parsed_so_far.append(part) + return parts + + @property + def supports_streaming(self) -> bool: + """Streaming is intentionally disabled for template expansion to keep architectures simple.""" + return False + + def decompile(self, val: Any) -> str: + """Decompiles structured A2UI payloads into raw format notation.""" + return self.underlying_parser.decompile(val) + + def wrap_decompiled_blocks(self, blocks: List[str]) -> str: + """Wraps multiple decompiled blocks with the format's enclosing tags.""" + return self.underlying_parser.wrap_decompiled_blocks(blocks) + + +@experimental +class TemplateInferenceFormat(InferenceFormat): + """Inference format providing template generation and synchronous expansion.""" + + def __init__( + self, + templates: Sequence[ + Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] + ], + catalog: Optional[Union[Catalog[Any, Any], A2uiCatalog, Dict[str, Any]]] = None, + allowed_primitives: Optional[List[str]] = None, + surface_id: str = "main", + version: str = "0.9.1", + underlying_format_factory: Optional[ + Callable[[A2uiCatalog, str, str], InferenceFormat] + ] = None, + ): + """Initializes the TemplateInferenceFormat with registered templates and base catalog. + + Args: + templates: List of registered Template definitions. + catalog: Optional base catalog instance or schema dictionary. + allowed_primitives: List of primitive components from the base + catalog to allow. + surface_id: Surface identifier for layout targeting. + version: Target A2UI protocol version ("0.9", "0.9.1", or "1.0"). + underlying_format_factory: Optional custom factory for underlying + InferenceFormat. + """ + self.templates = templates + self.surface_id = surface_id + self.version = _clean_version(version) + self.allowed_primitives = allowed_primitives + + # 1. Resolve base catalog + if catalog is None: + from a2ui.basic_catalog.provider import BasicCatalog + + config = BasicCatalog.get_config(self.version) + schema = config.provider.load() + s2c = load_from_bundled_resource( + self.version, SERVER_TO_CLIENT_SCHEMA_KEY, SPEC_VERSION_MAP + ) + common_types = load_from_bundled_resource( + self.version, COMMON_TYPES_SCHEMA_KEY, SPEC_VERSION_MAP + ) + self.base_catalog = A2uiCatalog( + version=self.version, + name="basic", + catalog_schema=schema, + s2c_schema=s2c, + common_types_schema=common_types, + ) + elif isinstance(catalog, A2uiCatalog): + self.base_catalog = catalog + elif hasattr(catalog, "catalog_schema"): + s2c = getattr(catalog, "s2c_schema", None) or load_from_bundled_resource( + self.version, SERVER_TO_CLIENT_SCHEMA_KEY, SPEC_VERSION_MAP + ) + common_types = getattr( + catalog, "common_types_schema", None + ) or load_from_bundled_resource( + self.version, COMMON_TYPES_SCHEMA_KEY, SPEC_VERSION_MAP + ) + self.base_catalog = A2uiCatalog( + version=self.version, + name="custom", + catalog_schema=getattr(catalog, "catalog_schema"), + s2c_schema=s2c, + common_types_schema=common_types, + ) + else: + s2c = load_from_bundled_resource( + self.version, SERVER_TO_CLIENT_SCHEMA_KEY, SPEC_VERSION_MAP + ) + common_types = load_from_bundled_resource( + self.version, COMMON_TYPES_SCHEMA_KEY, SPEC_VERSION_MAP + ) + self.base_catalog = A2uiCatalog( + version=self.version, + name="custom", + catalog_schema=catalog if isinstance(catalog, dict) else {}, + s2c_schema=s2c, + common_types_schema=common_types, + ) + + if self.allowed_primitives is None and hasattr( + self.base_catalog, "catalog_schema" + ): + schema_dict = getattr(self.base_catalog, "catalog_schema", {}) + if isinstance(schema_dict, dict) and "components" in schema_dict: + self.allowed_primitives = list(schema_dict["components"].keys()) + + # 2. Initialize TemplateProcessor + self.processor = TemplateProcessor( + templates=self.templates, + base_catalog=self.base_catalog, + version=self.version, + ) + + # 3. Construct synthetic A2uiCatalog containing allowed primitives + templates + self.synthetic_catalog_schema = self.processor.generate_inference_catalog( + allowed_primitives=self.allowed_primitives + ) + self.synthetic_catalog = A2uiCatalog( + version=self.version, + name="synthetic", + catalog_schema=self.synthetic_catalog_schema, + s2c_schema=self.base_catalog.s2c_schema, + common_types_schema=self.base_catalog.common_types_schema, + ) + + # 4. Initialize underlying inference format + express_version = f"v{self.version}" + if underlying_format_factory: + self._underlying_format = underlying_format_factory( + self.synthetic_catalog, self.surface_id, express_version + ) + else: + from a2ui.inference_formats.experimental.express.format import ( + ExpressFormat, + ) + + self._underlying_format = ExpressFormat( + catalog=self.synthetic_catalog, + surface_id=self.surface_id, + version=express_version, + ) + + # 5. Wrap parser with TemplateParser + self._parser = TemplateParser(self._underlying_format.parser, self.processor) + + @property + def parser(self) -> TemplateParser: + """The parser instance associated with the template inference format.""" + return self._parser + + @property + def prompt_generator(self) -> PromptGenerator: + """The prompt generator instance associated with the template inference format.""" + return self._underlying_format.prompt_generator + + +# Backward-compatibility alias +A2uiTemplateManager = TemplateInferenceFormat diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/template/manager.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py similarity index 100% rename from agent_sdks/python/a2ui_agent/src/a2ui/template/manager.py rename to agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py new file mode 100644 index 0000000000..22b45f5b76 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -0,0 +1,869 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Data models for A2UI Template definitions, typed parameters, expressions, and dynamic resolvers.""" + +from __future__ import annotations + +import asyncio +from enum import Enum +import inspect +import json +import os +from pathlib import Path +import re +from dataclasses import dataclass, field +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +import jsonschema +import yaml + +SCHEMA_PATH = Path(__file__).parent / "schema" / "template_definition.json" +with open(SCHEMA_PATH, "r", encoding="utf-8") as _f: + TEMPLATE_DEFINITION_SCHEMA = json.load(_f) + + +class ParamType(str, Enum): + """Supported A2UI semantic parameter types.""" + + STRING = "string" + NUMBER = "number" + INTEGER = "integer" + BOOLEAN = "boolean" + ENUM = "enum" + OBJECT = "object" + ARRAY = "array" + CHILD = "child" + CHILDREN = "children" + ACTION = "action" + + +@dataclass +class Param: + """Strongly typed template parameter definition.""" + + type: Union[ParamType, str] + title: Optional[str] = None + description: Optional[str] = None + default: Optional[Any] = None + values: Optional[List[str]] = None + properties: Optional[Dict[str, Any]] = None + items: Optional[Union[Param, Dict[str, Any], str]] = None + required: bool = True + minimum: Optional[float] = None + maximum: Optional[float] = None + + @classmethod + def string( + cls, + description: Optional[str] = None, + title: Optional[str] = None, + default: Optional[str] = None, + ) -> Param: + return cls( + type=ParamType.STRING, + description=description, + title=title, + default=default, + ) + + @classmethod + def number( + cls, + description: Optional[str] = None, + title: Optional[str] = None, + default: Optional[float] = None, + ) -> Param: + return cls( + type=ParamType.NUMBER, + description=description, + title=title, + default=default, + ) + + @classmethod + def integer( + cls, + description: Optional[str] = None, + title: Optional[str] = None, + default: Optional[int] = None, + ) -> Param: + return cls( + type=ParamType.INTEGER, + description=description, + title=title, + default=default, + ) + + @classmethod + def boolean( + cls, + description: Optional[str] = None, + title: Optional[str] = None, + default: Optional[bool] = None, + ) -> Param: + return cls( + type=ParamType.BOOLEAN, + description=description, + title=title, + default=default, + ) + + @classmethod + def enum( + cls, + values: List[str], + description: Optional[str] = None, + title: Optional[str] = None, + default: Optional[str] = None, + ) -> Param: + return cls( + type=ParamType.ENUM, + values=values, + description=description, + title=title, + default=default, + ) + + @classmethod + def child( + cls, description: Optional[str] = None, title: Optional[str] = None + ) -> Param: + return cls(type=ParamType.CHILD, description=description, title=title) + + @classmethod + def children( + cls, description: Optional[str] = None, title: Optional[str] = None + ) -> Param: + return cls( + type=ParamType.CHILDREN, + description=description, + title=title, + default=[], + ) + + @classmethod + def action( + cls, description: Optional[str] = None, title: Optional[str] = None + ) -> Param: + return cls(type=ParamType.ACTION, description=description, title=title) + + @classmethod + def array( + cls, + items: Union[Param, Dict[str, Any], str], + description: Optional[str] = None, + title: Optional[str] = None, + ) -> Param: + return cls( + type=ParamType.ARRAY, items=items, description=description, title=title + ) + + @classmethod + def object( + cls, + properties: Dict[str, Any], + description: Optional[str] = None, + title: Optional[str] = None, + ) -> Param: + return cls( + type=ParamType.OBJECT, + properties=properties, + description=description, + title=title, + ) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> Param: + raw_type = data.get("type", "string") + p_type = ( + ParamType(raw_type) + if raw_type in ParamType._value2member_map_ + else raw_type + ) + return cls( + type=p_type, + title=data.get("title"), + description=data.get("description"), + default=data.get("default"), + values=data.get("values"), + properties=data.get("properties"), + items=data.get("items"), + required=data.get("required", True), + minimum=data.get("minimum"), + maximum=data.get("maximum"), + ) + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = { + "type": ( + self.type.value if isinstance(self.type, ParamType) else str(self.type) + ) + } + if self.title: + res["title"] = self.title + if self.description: + res["description"] = self.description + if self.default is not None: + res["default"] = self.default + if self.values is not None: + res["values"] = self.values + if self.properties is not None: + res["properties"] = { + k: v.to_dict() if isinstance(v, Param) else v + for k, v in self.properties.items() + } + if self.items is not None: + res["items"] = ( + self.items.to_dict() if isinstance(self.items, Param) else self.items + ) + if self.minimum is not None: + res["minimum"] = self.minimum + if self.maximum is not None: + res["maximum"] = self.maximum + return res + + +@dataclass +class ParamRef: + """Direct reference to a parameter or dot path.""" + + param: str + default: Optional[Any] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = {"param": self.param} + if self.default is not None: + res["default"] = self.default + return res + + +@dataclass +class Concat: + """String concatenation expression of literals and parameter references.""" + + concat: List[Union[str, ParamRef, Dict[str, Any]]] + + def to_dict(self) -> Dict[str, Any]: + return { + "concat": [ + (p.to_dict() if isinstance(p, ParamRef) else p) for p in self.concat + ] + } + + +@dataclass +class FormatExpr: + """String formatting expression with {key} arguments.""" + + format: str + args: Dict[str, Union[str, ParamRef, Dict[str, Any]]] + + def to_dict(self) -> Dict[str, Any]: + return { + "format": self.format, + "args": { + k: v.to_dict() if isinstance(v, ParamRef) else v + for k, v in self.args.items() + }, + } + + +@dataclass +class TemplateLoop: + """Array mapping parameter to child template instances or inline item layouts.""" + + param: str + template: Optional[str] = None + item: Optional[List[Union[TemplateComponent, Dict[str, Any]]]] = None + as_var: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = {"param": self.param} + if self.template: + res["template"] = self.template + if self.item: + res["item"] = [ + (c.to_dict() if isinstance(c, TemplateComponent) else c) + for c in self.item + ] + if self.as_var: + res["as"] = self.as_var + return res + + +@dataclass +class TemplateComponent: + """Strongly typed template component definition.""" + + id: str + component: str + properties: Dict[str, Any] = field(default_factory=dict) + child: Optional[Union[str, ParamRef, Dict[str, Any]]] = None + children: Optional[Union[List[Any], ParamRef, TemplateLoop, Dict[str, Any]]] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = {"id": self.id, "component": self.component} + if self.child is not None: + res["child"] = ( + self.child.to_dict() if isinstance(self.child, ParamRef) else self.child + ) + if self.children is not None: + if isinstance(self.children, (ParamRef, TemplateLoop)): + res["children"] = self.children.to_dict() + else: + res["children"] = self.children + for k, v in self.properties.items(): + if isinstance(v, (ParamRef, Concat, FormatExpr, TemplateLoop)): + res[k] = v.to_dict() + else: + res[k] = v + return res + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> TemplateComponent: + data_copy = dict(data) + c_id = data_copy.pop("id") + c_type = data_copy.pop("component") + child = data_copy.pop("child", None) + children = data_copy.pop("children", None) + return cls( + id=c_id, + component=c_type, + properties=data_copy, + child=child, + children=children, + ) + + +def normalize_node(node: Any) -> Any: + """Polymorphically converts builder objects, dataclasses, or Pydantic models into dicts.""" + if node is None: + return None + if hasattr(node, "to_dict") and callable(node.to_dict): + return node.to_dict() + if hasattr(node, "model_dump") and callable(node.model_dump): + return node.model_dump(exclude_none=True, by_alias=True) + import dataclasses + + if dataclasses.is_dataclass(node) and not isinstance(node, type): + return dataclasses.asdict(node) + return node + + +def flatten_nested_layout( + node: Any, + parent_id: Optional[str] = None, + slot_name: Optional[str] = None, + index: Optional[int] = None, +) -> Tuple[str, List[Dict[str, Any]]]: + """Recursively flattens a nested layout tree (dicts, builder objects) into a flat component graph with synthetic IDs. + + Returns: + Tuple of (assigned_component_id, list_of_flattened_components) + """ + node = normalize_node(node) + if not isinstance(node, dict): + return str(node), [] + + node_copy = dict(node) + comp_type = node_copy.get("component", "Node").lower() + + # 1. Determine or generate synthetic component ID + if "id" in node_copy and node_copy["id"]: + node_id = str(node_copy["id"]) + elif parent_id is None: + node_id = "root" + elif index is not None: + node_id = f"{parent_id}_{slot_name}_{index}_{comp_type}" + else: + node_id = f"{parent_id}_{slot_name}_{comp_type}" + + node_copy["id"] = node_id + flat_components: List[Dict[str, Any]] = [] + + # 2. Process nested 'child' + if "child" in node_copy: + child_val = normalize_node(node_copy["child"]) + if isinstance(child_val, dict) and "component" in child_val: + child_id, sub_comps = flatten_nested_layout( + child_val, parent_id=node_id, slot_name="child" + ) + node_copy["child"] = child_id + flat_components.extend(sub_comps) + else: + node_copy["child"] = child_val + + # 3. Process nested 'children' + if "children" in node_copy: + children_val = normalize_node(node_copy["children"]) + if isinstance(children_val, list): + flattened_child_ids: List[Any] = [] + for idx, raw_item in enumerate(children_val): + item = normalize_node(raw_item) + if isinstance(item, dict) and "component" in item: + item_id, sub_comps = flatten_nested_layout( + item, parent_id=node_id, slot_name="child", index=idx + ) + flattened_child_ids.append(item_id) + flat_components.extend(sub_comps) + elif isinstance(item, dict) and "loop" in item: + # Loop inside children list + loop_cfg = item["loop"] + loop_dict: Dict[str, Any] = {"param": loop_cfg.get("param")} + if "as" in loop_cfg: + loop_dict["as"] = loop_cfg["as"] + if "item" in loop_cfg and isinstance(loop_cfg["item"], dict): + _, item_sub_comps = flatten_nested_layout( + loop_cfg["item"], parent_id=f"{node_id}_item" + ) + loop_dict["item"] = item_sub_comps + elif "template" in loop_cfg: + loop_dict["template"] = loop_cfg["template"] + flattened_child_ids.append(loop_dict) + else: + flattened_child_ids.append(item) + node_copy["children"] = flattened_child_ids + elif isinstance(children_val, dict) and "loop" in children_val: + loop_cfg = children_val["loop"] + loop_dict = {"param": loop_cfg.get("param")} + if "as" in loop_cfg: + loop_dict["as"] = loop_cfg["as"] + if "item" in loop_cfg and isinstance(loop_cfg["item"], dict): + _, item_sub_comps = flatten_nested_layout( + loop_cfg["item"], parent_id=f"{node_id}_item" + ) + loop_dict["item"] = item_sub_comps + elif "template" in loop_cfg: + loop_dict["template"] = loop_cfg["template"] + node_copy["children"] = loop_dict + else: + node_copy["children"] = children_val + + flat_components.insert(0, node_copy) + return node_id, flat_components + + +def normalize_a2ui_type_to_jsonschema(meta: Any) -> Any: + """Recursively converts an A2UI parameter type schema into standard JSON Schema.""" + if isinstance(meta, Param): + meta = meta.to_dict() + if not isinstance(meta, dict): + return meta + schema = dict(meta) + p_type = schema.get("type") + if p_type == "enum" and "values" in schema: + schema["type"] = "string" + schema["enum"] = schema.pop("values") + elif p_type in ["child", "action"]: + schema["type"] = "string" + elif p_type == "children": + schema["type"] = "array" + schema["items"] = {"type": "string"} + + if "properties" in schema and isinstance(schema["properties"], dict): + schema["properties"] = { + k: normalize_a2ui_type_to_jsonschema(v) + for k, v in schema["properties"].items() + } + if "items" in schema: + if isinstance(schema["items"], (dict, Param)): + schema["items"] = normalize_a2ui_type_to_jsonschema(schema["items"]) + elif isinstance(schema["items"], str): + schema["items"] = {"type": schema["items"]} + return schema + + +@dataclass +class BaseTemplate: + """Base class for all templates.""" + + template_id: str + version: str = "0.1" + description: Optional[str] = None + sample_data: Optional[Dict[str, Any]] = None + is_dynamic: bool = False + + +@dataclass +class StaticTemplate(BaseTemplate): + """Declarative A2UI template definition authored in YAML with layout trees and parameter substitutions.""" + + parameters: Dict[str, Union[Param, Dict[str, Any]]] = field(default_factory=dict) + components: List[Union[TemplateComponent, Dict[str, Any]]] = field( + default_factory=list + ) + raw_layout: Optional[Dict[str, Any]] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> StaticTemplate: + """Constructs a StaticTemplate instance from a dictionary payload.""" + template_id = data.get("templateId") or data.get("template_id") + if not template_id: + raise ValueError("Template dictionary must contain 'templateId'.") + + version = data.get("version", "0.1") + if version != "0.1": + raise ValueError( + f"Unsupported template version '{version}'. Currently only '0.1' is" + " supported." + ) + + raw_params = data.get("parameters", {}) + parsed_params: Dict[str, Union[Param, Dict[str, Any]]] = {} + for k, v in raw_params.items(): + parsed_params[k] = Param.from_dict(v) if isinstance(v, dict) else v + + # Flatten nested layout if provided + raw_layout = data.get("layout") + components: List[Union[TemplateComponent, Dict[str, Any]]] = [] + if raw_layout and isinstance(raw_layout, dict): + _, flat_comps = flatten_nested_layout(raw_layout) + components = list(flat_comps) + elif "components" in data and isinstance(data["components"], list): + components = list(data["components"]) + else: + raise ValueError( + f"Template '{template_id}' must declare a 'layout' tree definition." + ) + + instance = cls( + template_id=template_id, + version=version, + parameters=parsed_params, + components=components, + raw_layout=raw_layout, + sample_data=data.get("sampleData") or data.get("sample_data"), + description=data.get("description"), + is_dynamic=False, + ) + instance.validate_definition() + return instance + + @classmethod + def from_yaml_string(cls, yaml_content: str) -> List[StaticTemplate]: + """Parses a YAML string containing one or more '---' separated template documents.""" + docs = list(yaml.safe_load_all(yaml_content)) + templates: List[StaticTemplate] = [] + for doc in docs: + if not doc or not isinstance(doc, dict): + continue + templates.append(cls.from_dict(doc)) + if not templates: + raise ValueError("No valid template definitions found in YAML content.") + return templates + + @classmethod + def from_yaml_file(cls, file_path: Union[str, Path]) -> List[StaticTemplate]: + """Loads and parses all Template definitions from a YAML file.""" + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + return cls.from_yaml_string(content) + + @classmethod + def from_yaml(cls, yaml_content: str) -> StaticTemplate: + """Convenience method to load a single template definition from YAML.""" + templates = cls.from_yaml_string(yaml_content) + return templates[0] + + def to_dict(self) -> Dict[str, Any]: + """Serializes the template instance to a dictionary.""" + params_dict: Dict[str, Any] = {} + for k, v in self.parameters.items(): + params_dict[k] = v.to_dict() if isinstance(v, Param) else v + + comps_list: List[Dict[str, Any]] = [] + for c in self.components: + comps_list.append(c.to_dict() if isinstance(c, TemplateComponent) else c) + + res: Dict[str, Any] = { + "version": self.version, + "templateId": self.template_id, + "parameters": params_dict, + } + if self.raw_layout is not None: + res["layout"] = self.raw_layout + else: + res["components"] = comps_list + + if self.description: + res["description"] = self.description + if self.sample_data is not None: + res["sampleData"] = self.sample_data + return res + + def to_yaml(self) -> str: + """Serializes the template instance to a clean YAML string.""" + return yaml.dump(self.to_dict(), sort_keys=False) + + @staticmethod + def _collect_loop_vars(val: Any, loop_vars: set) -> None: + if isinstance(val, dict): + if "as" in val and isinstance(val["as"], str): + loop_vars.add(val["as"]) + for v in val.values(): + StaticTemplate._collect_loop_vars(v, loop_vars) + elif isinstance(val, list): + for item in val: + StaticTemplate._collect_loop_vars(item, loop_vars) + + def _check_val(self, val: Any, loop_vars: set) -> None: + expr_pattern = re.compile(r"\$\{([\w\.]+)\}") + if isinstance(val, dict): + if "param" in val and isinstance(val["param"], str): + self._check_path(val["param"], loop_vars) + if "concat" in val and isinstance(val["concat"], list): + for elem in val["concat"]: + self._check_val(elem, loop_vars) + if "format" in val and "args" in val and isinstance(val["args"], dict): + for v in val["args"].values(): + self._check_val(v, loop_vars) + for v in val.values(): + self._check_val(v, loop_vars) + elif isinstance(val, list): + for item in val: + self._check_val(item, loop_vars) + elif isinstance(val, str): + for m in expr_pattern.finditer(val): + self._check_path(m.group(1), loop_vars) + + def _check_path(self, path: str, loop_vars: set) -> None: + parts = path.split(".") + root = parts[0] + if root not in self.parameters: + if root.startswith("__") or root in loop_vars: + return + raise ValueError( + f"Template '{self.template_id}': Component references parameter" + f" '{root}' in '{path}', but it is not declared in template" + " parameters." + ) + + curr_schema = self.parameters[root] + if isinstance(curr_schema, Param): + curr_schema = curr_schema.to_dict() + for part in parts[1:]: + if isinstance(curr_schema, dict) and "properties" in curr_schema: + props = curr_schema.get("properties", {}) + if part not in props: + raise ValueError( + f"Template '{self.template_id}': Component references" + f" property '{part}' in '{path}', but it is not declared in" + f" parameter '{root}' properties." + ) + curr_schema = props[part] + + def validate_definition(self) -> None: + """Statically validates the template against the JSON Schema, verifies parameter references, and validates sampleData.""" + raw_dict = self.to_dict() + try: + jsonschema.validate(instance=raw_dict, schema=TEMPLATE_DEFINITION_SCHEMA) + except jsonschema.ValidationError as err: + raise ValueError( + f"Template '{self.template_id}' fails template_definition.json schema:" + f" {err.message}" + ) from err + + loop_vars = {"item"} + for comp in self.components: + comp_dict = comp.to_dict() if isinstance(comp, TemplateComponent) else comp + self._collect_loop_vars(comp_dict, loop_vars) + + for comp in self.components: + comp_dict = comp.to_dict() if isinstance(comp, TemplateComponent) else comp + self._check_val(comp_dict, loop_vars) + + if self.sample_data and isinstance(self.sample_data, dict): + for p_name, p_val in self.sample_data.items(): + if p_name in self.parameters: + p_meta = self.parameters[p_name] + if isinstance(p_meta, Param): + p_type = ( + p_meta.type.value + if isinstance(p_meta.type, ParamType) + else str(p_meta.type) + ) + else: + p_type = str(p_meta.get("type", "string")) + if p_type in ["child", "children", "action"]: + continue + + val_schema = normalize_a2ui_type_to_jsonschema(p_meta) + try: + jsonschema.validate(instance=p_val, schema=val_schema) + except jsonschema.ValidationError as err: + raise ValueError( + f"Template '{self.template_id}': sampleData for parameter" + f" '{p_name}' fails validation: {err.message}" + ) from err + + +# Retain Template alias for backward compatibility +Template = StaticTemplate + + +class DynamicTemplate(BaseTemplate): + """Programmatic template that either executes a data resolver for a static layout or runs a render function returning an AST.""" + + def __init__( + self, + template_id: str, + render: Optional[Callable[..., Any]] = None, + resolver: Optional[Callable[..., Any]] = None, + layout: Optional[Union[StaticTemplate, str, Dict[str, Any]]] = None, + parameters: Optional[Dict[str, Union[Param, Dict[str, Any]]]] = None, + description: Optional[str] = None, + sample_data: Optional[Dict[str, Any]] = None, + render_fn: Optional[Callable[..., Any]] = None, + version: str = "0.1", + ): + super().__init__( + template_id=template_id, + version=version, + description=description, + sample_data=sample_data, + is_dynamic=True, + ) + self.render_fn = render or render_fn + self.resolver = resolver + self.layout: Optional[StaticTemplate] = None + + if layout is not None: + if isinstance(layout, str): + if os.path.exists(layout): + self.layout = StaticTemplate.from_yaml_file(layout)[0] + else: + self.layout = StaticTemplate.from_yaml(layout) + elif isinstance(layout, dict): + self.layout = StaticTemplate.from_dict(layout) + else: + self.layout = layout + + target_fn = self.render_fn or self.resolver + if target_fn is None and self.layout is None: + raise ValueError( + f"DynamicTemplate '{template_id}' must provide either a 'render'" + " function or a ('resolver', 'layout') pair." + ) + + if parameters is not None: + self.parameters = parameters + elif target_fn is not None: + self.parameters = self._infer_parameters_from_resolver(target_fn) + else: + self.parameters = {} + + @staticmethod + def _infer_parameters_from_resolver( + fn: Callable[..., Any], + ) -> Dict[str, Union[Param, Dict[str, Any]]]: + """Automatically derives parameter definitions from the Python function signature.""" + sig = inspect.signature(fn) + inferred: Dict[str, Union[Param, Dict[str, Any]]] = {} + type_map = { + str: ParamType.STRING, + int: ParamType.INTEGER, + float: ParamType.NUMBER, + bool: ParamType.BOOLEAN, + } + for name, p in sig.parameters.items(): + if name in ["self", "context"] or p.kind in ( + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD, + ): + continue + p_type = type_map.get(p.annotation, ParamType.STRING) + default = p.default if p.default is not inspect.Parameter.empty else None + inferred[name] = Param( + type=p_type, + default=default, + description=f"Input parameter '{name}' for dynamic resolver.", + ) + return inferred + + def resolve(self, passed_params: Dict[str, Any]) -> Dict[str, Any]: + """Executes the resolver synchronously or asynchronously.""" + if self.resolver is None: + return {} + if isinstance(passed_params, dict): + sig = inspect.signature(self.resolver) + has_kwargs = any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + if has_kwargs: + filtered_params = passed_params + else: + filtered_params = { + k: v for k, v in passed_params.items() if k in sig.parameters + } + res = self.resolver(**filtered_params) + elif isinstance(passed_params, list): + res = self.resolver(*passed_params) + else: + res = self.resolver(passed_params) + + if asyncio.iscoroutine(res): + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + try: + import nest_asyncio # type: ignore + + nest_asyncio.apply() + except ImportError as err: + raise RuntimeError( + "An event loop is already running. Please install" + " 'nest-asyncio' to run async resolvers synchronously." + ) from err + res_val = loop.run_until_complete(res) + else: + res_val = loop.run_until_complete(res) + except RuntimeError as err: + if "already running" in str(err) or "run_until_complete" in str(err): + raise + res_val = asyncio.run(res) + except Exception: + res_val = asyncio.run(res) + return dict(res_val) if isinstance(res_val, dict) else {} + return dict(res) if isinstance(res, dict) else {} + + def to_dict(self) -> Dict[str, Any]: + params_dict: Dict[str, Any] = {} + for k, v in self.parameters.items(): + params_dict[k] = v.to_dict() if isinstance(v, Param) else v + + comps_list = [] + if self.layout is not None: + comps_list = [ + (c.to_dict() if isinstance(c, TemplateComponent) else c) + for c in self.layout.components + ] + + res: Dict[str, Any] = { + "version": self.version, + "templateId": self.template_id, + "parameters": params_dict, + "components": comps_list, + "isDynamic": True, + } + if self.description: + res["description"] = self.description + if self.sample_data is not None: + res["sampleData"] = self.sample_data + return res + + def to_yaml(self) -> str: + """Serializes the dynamic template or underlying layout to a clean YAML string.""" + if self.layout is not None: + return self.layout.to_yaml() + return yaml.dump(self.to_dict(), sort_keys=False) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py new file mode 100644 index 0000000000..337c49e558 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py @@ -0,0 +1,917 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core processing, expansion engine, and dynamic resolver execution for A2UI Templates.""" + +from __future__ import annotations + +import asyncio +import inspect +import re +from typing import ( + Any, + Dict, + List, + Mapping, + Optional, + Sequence, + Tuple, + TypeAlias, + Union, +) +import jsonschema +from .models import ( + Template, + StaticTemplate, + DynamicTemplate, + BaseTemplate, + Param, + ParamType, + ParamRef, + Concat, + FormatExpr, + TemplateLoop, + TemplateComponent, + normalize_a2ui_type_to_jsonschema, + flatten_nested_layout, +) + +# --------------------------------------------------------------------------- +# Strong Semantic Type Aliases +# --------------------------------------------------------------------------- + +# 1. Identifier Types +TemplateId: TypeAlias = str +InstanceId: TypeAlias = str +ComponentId: TypeAlias = str +ParamPath: TypeAlias = str + +# 2. Template Evaluation Types +TemplateParams: TypeAlias = Mapping[str, Any] +TemplateNodeDict: TypeAlias = Dict[str, Any] + +# 3. Standard A2UI Wire-Format Output Types +A2UIComponent: TypeAlias = Dict[str, Any] +A2UIComponentList: TypeAlias = List[A2UIComponent] +A2UIMessage: TypeAlias = Dict[str, Any] + +# 4. Catalog and Schema Types +CatalogSchema: TypeAlias = Dict[str, Any] +JSONSchemaDict: TypeAlias = Dict[str, Any] + +__all__ = [ + "TemplateProcessor", + "TemplateId", + "InstanceId", + "ComponentId", + "ParamPath", + "TemplateParams", + "TemplateNodeDict", + "A2UIComponent", + "A2UIComponentList", + "A2UIMessage", + "CatalogSchema", + "JSONSchemaDict", +] + + +def _resolve_param_path(path: ParamPath, params: TemplateParams) -> Tuple[bool, Any]: + """Resolves a dot-separated parameter path (e.g. 'user.name' or 'user.metrics.count') from params dict.""" + parts = path.split(".") + curr: Any = params + for part in parts: + if isinstance(curr, (dict, Mapping)) and part in curr: + curr = curr[part] + elif ( + isinstance(curr, (list, tuple)) and part.isdigit() and int(part) < len(curr) + ): + curr = curr[int(part)] + else: + return False, None + return True, curr + + +def _substitute_params(val: Any, params: TemplateParams) -> Any: + """Recursively replaces parameter references with their resolved values.""" + if isinstance(val, (ParamRef, Concat, FormatExpr, TemplateLoop)): + val = val.to_dict() + + if isinstance(val, dict): + # 1. Direct param reference: {"param": "userName", "default": ...} + if ( + "param" in val + and isinstance(val["param"], str) + and "template" not in val + and "item" not in val + ): + param_path = val["param"] + found, res = _resolve_param_path(param_path, params) + if found and res is not None: + return res + if "default" in val: + return val["default"] + return None + + # 2. String concatenation: {"concat": ["Strategic Objectives: ", {"param": "teamName"}]} + if "concat" in val and isinstance(val["concat"], list): + parts = [] + for item in val["concat"]: + sub = _substitute_params(item, params) + if sub is not None: + parts.append(str(sub)) + return "".join(parts) + + # 3. String formatting: {"format": "Competency: {name}", "args": {...}} + if "format" in val and "args" in val and isinstance(val["args"], dict): + fmt_str = str(val["format"]) + args_sub = { + k: _substitute_params(v, params) for k, v in val["args"].items() + } + try: + return fmt_str.format(**args_sub) + except Exception: + return fmt_str + + # 4. Standard dictionary traversal + return {k: _substitute_params(v, params) for k, v in val.items()} + + elif isinstance(val, list): + return [_substitute_params(x, params) for x in val] + + elif isinstance(val, str): + # Exact parameter match preserving type: e.g. "${user}" or "${score}" + match = re.match(r"^\$\{([\w\.]+)\}$", val) + if match: + param_path = match.group(1) + found, res = _resolve_param_path(param_path, params) + if found: + return res + + # In-string placeholder replacement: replace all occurrences of ${path} + pattern = re.compile(r"\$\{([\w\.]+)\}") + + def replacer(m: re.Match[str]) -> str: + path = m.group(1) + found, res = _resolve_param_path(path, params) + if found and res is not None: + return str(res) + return str(m.group(0)) + + return str(pattern.sub(replacer, val)) + + return val + + +# Private aliases for internal backwards-compatibility if referenced +resolve_param_path = _resolve_param_path +substitute_params = _substitute_params + + +class TemplateProcessor: + """Manages template registration, synthetic catalog compilation, and payload expansion.""" + + def __init__( + self, + templates: Sequence[ + Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] + ], + base_catalog: Optional[Union[CatalogSchema, Any]] = None, + version: str = "v0.9.1", + ): + """Initializes the TemplateProcessor. + + Args: + templates: List of registered Template definitions. + base_catalog: Optional base catalog schema dict or A2uiCatalog instance. + If omitted, the processor operates independently without catalog assumptions. + version: Target A2UI protocol version ("v0.9", "v0.9.1", or "v1.0"). + """ + self.templates: Dict[ + TemplateId, Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] + ] = {t.template_id: t for t in templates} + self.version = version + self.base_catalog: Optional[CatalogSchema] = None + self.base_catalog_id: Optional[str] = None + + if base_catalog is not None: + if hasattr(base_catalog, "catalog_schema") and isinstance( + base_catalog.catalog_schema, dict + ): + self.base_catalog = base_catalog.catalog_schema + elif isinstance(base_catalog, dict): + self.base_catalog = base_catalog + + if self.base_catalog is not None: + self.base_catalog_id = self.base_catalog.get( + "$id" + ) or self.base_catalog.get("id") + + self._validate_templates() + + def generate_inference_catalog( + self, + allowed_primitives: Optional[Sequence[str]] = None, + ) -> CatalogSchema: + """Generates a synthetic JSON catalog containing allowed primitives and all registered templates.""" + if allowed_primitives is None: + if self.base_catalog is not None: + allowed_primitives = list( + self.base_catalog.get("components", {}).keys() + ) + else: + allowed_primitives = [] + + catalog_id = ( + self.base_catalog_id + if self.base_catalog_id + else "https://a2ui.org/catalog/synthetic" + ) + + synthetic_catalog: CatalogSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": catalog_id, + "catalogId": catalog_id, + "title": "A2UI Synthetic Inference Catalog", + "components": {}, + "functions": ( + self.base_catalog.get("functions", {}) + if self.base_catalog is not None + else {} + ), + "$defs": { + "anyComponent": { + "oneOf": [], + "discriminator": {"propertyName": "component"}, + }, + "anyFunction": ( + self.base_catalog.get("$defs", {}).get("anyFunction", {}) + if self.base_catalog is not None + else {} + ), + }, + } + + # 1. Copy over allowed primitives from the base catalog if present + if self.base_catalog is not None: + base_components = self.base_catalog.get("components", {}) + for prim in allowed_primitives: + if prim in base_components: + synthetic_catalog["components"][prim] = base_components[prim] + synthetic_catalog["$defs"]["anyComponent"]["oneOf"].append( + {"$ref": f"#/components/{prim}"} + ) + + # 2. Add registered custom templates as high-level component schemas + common_types_url = ( + "https://a2ui.org/specification/v0_9/common_types.json" + if "0.9" in self.version + else "https://a2ui.org/specification/v1_0/common_types.json" + ) + + for t_id, template in self.templates.items(): + param_required = [] + properties_dict: Dict[str, Any] = {"component": {"const": t_id}} + + raw_params = ( + template.parameters.items() if hasattr(template, "parameters") else {} + ) + + for p_name, p_meta in raw_params: + p_meta_dict = p_meta.to_dict() if isinstance(p_meta, Param) else p_meta + properties_dict[p_name] = self._promote_parameter(p_meta_dict) + p_type = ( + p_meta.type.value + if isinstance(p_meta, Param) and isinstance(p_meta.type, ParamType) + else ( + p_meta.get("type") + if isinstance(p_meta, dict) + else getattr(p_meta, "type", None) + ) + ) + has_default = ( + p_meta.default is not None + if isinstance(p_meta, Param) + else (isinstance(p_meta, dict) and "default" in p_meta) + ) + if not has_default and p_type != "array": + param_required.append(p_name) + + template_schema = { + "type": "object", + "allOf": [ + {"$ref": f"{common_types_url}#/$defs/ComponentCommon"}, + { + "type": "object", + "properties": properties_dict, + "required": ["component"] + param_required, + }, + ], + "unevaluatedProperties": False, + } + if template.description: + template_schema["description"] = template.description + + synthetic_catalog["components"][t_id] = template_schema + synthetic_catalog["$defs"]["anyComponent"]["oneOf"].append( + {"$ref": f"#/components/{t_id}"} + ) + + return synthetic_catalog + + def expand_template( + self, + instance_id: InstanceId, + template_id: TemplateId, + passed_params: TemplateParams, + _depth: int = 0, + _call_stack: Optional[List[TemplateId]] = None, + ) -> A2UIComponentList: + """Recursively expands a template instance into standard primitive components.""" + if _call_stack is None: + _call_stack = [] + + template = self.templates.get(template_id) + if not template: + available = list(self.templates.keys()) + raise ValueError( + f"Template '{template_id}' is not registered. Available: {available}" + ) + + if template_id in _call_stack: + raise ValueError( + f"Circular template reference detected: {' -> '.join(_call_stack)} ->" + f" {template_id}" + ) + + if _depth > 32: + raise RecursionError( + f"Maximum template expansion depth exceeded ({_depth}) at" + f" '{template_id}'. Call stack: {' -> '.join(_call_stack)}" + ) + + current_stack = _call_stack + [template_id] + + # 1. Handle DynamicTemplate execution + if getattr(template, "is_dynamic", False): + dynamic_tmpl: DynamicTemplate = template # type: ignore + + # Mode A: Programmatic Render Function (returns AST tree or list directly) + if dynamic_tmpl.render_fn is not None: + if isinstance(passed_params, (dict, Mapping)): + sig = inspect.signature(dynamic_tmpl.render_fn) + has_kwargs = any( + p.kind == inspect.Parameter.VAR_KEYWORD + for p in sig.parameters.values() + ) + if has_kwargs: + filtered_params = dict(passed_params) + else: + filtered_params = { + k: v + for k, v in passed_params.items() + if k in sig.parameters + } + res = dynamic_tmpl.render_fn(**filtered_params) + elif isinstance(passed_params, list): + res = dynamic_tmpl.render_fn(*passed_params) + else: + res = dynamic_tmpl.render_fn(passed_params) + if asyncio.iscoroutine(res): + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + try: + import nest_asyncio # type: ignore + + nest_asyncio.apply() + except ImportError as err: + raise RuntimeError( + "An event loop is already running. Please install" + " 'nest-asyncio' to run async render functions" + " synchronously." + ) from err + res_tree = loop.run_until_complete(res) + else: + res_tree = loop.run_until_complete(res) + except RuntimeError as err: + if "already running" in str(err) or "run_until_complete" in str( + err + ): + raise + res_tree = asyncio.run(res) + except Exception: + res_tree = asyncio.run(res) + else: + res_tree = res + + if isinstance(res_tree, list): + flattened_list: A2UIComponentList = [] + for item in res_tree: + _, comps = flatten_nested_layout(item, parent_id=instance_id) + flattened_list.extend(comps) + return flattened_list + else: + _, flat_components = flatten_nested_layout(res_tree) + # Normalize root ID to instance_id + result: A2UIComponentList = [] + for c in flat_components: + c_copy = dict(c) + if c_copy["id"] == "root": + c_copy["id"] = instance_id + elif c_copy["id"].startswith("root_"): + c_copy["id"] = f"{instance_id}_{c_copy['id'][5:]}" + if "child" in c_copy and isinstance(c_copy["child"], str): + if c_copy["child"] == "root": + c_copy["child"] = instance_id + elif c_copy["child"].startswith("root_"): + c_copy["child"] = f"{instance_id}_{c_copy['child'][5:]}" + if "children" in c_copy and isinstance( + c_copy["children"], list + ): + c_copy["children"] = [ + ( + instance_id + if ch == "root" + else ( + f"{instance_id}_{ch[5:]}" + if isinstance(ch, str) + and ch.startswith("root_") + else ch + ) + ) + for ch in c_copy["children"] + ] + result.append(c_copy) + return result + + # Mode B: Resolver + Static Layout Binding + elif dynamic_tmpl.layout is not None: + resolved_data = dynamic_tmpl.resolve(dict(passed_params)) + combined_params = {**dict(passed_params), **resolved_data} + layout = dynamic_tmpl.layout + return self._expand_static_layout( + instance_id, + layout, + combined_params, + _depth=_depth + 1, + _call_stack=current_stack, + ) + + # 2. Handle StaticTemplate execution + return self._expand_static_layout( + instance_id, + template, + passed_params, + _depth=_depth + 1, + _call_stack=current_stack, + ) + + @staticmethod + def _map_id(internal_id: str, instance_id: InstanceId) -> ComponentId: + if internal_id == "root": + return instance_id + return f"{instance_id}_{internal_id}" + + @staticmethod + def _resolve_slot(val: Any, params: TemplateParams) -> Tuple[bool, Any]: + if isinstance(val, (ParamRef, TemplateLoop)): + val = val.to_dict() + if ( + isinstance(val, dict) + and "param" in val + and "template" not in val + and "item" not in val + ): + p_path = val["param"] + found, res = _resolve_param_path(p_path, params) + if found: + return True, res + if "default" in val: + return True, val["default"] + elif isinstance(val, str): + if val.startswith("${") and val.endswith("}"): + p_path = val[2:-1] + found, res = _resolve_param_path(p_path, params) + if found: + return True, res + elif val.startswith("__PARAM__"): + p_name = val[9:] + found, res = _resolve_param_path(p_name, params) + if found: + return True, res + return False, val + + @classmethod + def _map_child_list( + cls, child_list: Any, instance_id: InstanceId, params: TemplateParams + ) -> Any: + if isinstance(child_list, list): + res_list: List[Any] = [] + for c_id in child_list: + is_slot, slot_val = cls._resolve_slot(c_id, params) + if is_slot: + if isinstance(slot_val, list): + res_list.extend(slot_val) + elif slot_val is not None: + res_list.append(slot_val) + else: + res_list.append( + cls._map_id(c_id, instance_id) + if isinstance(c_id, str) + else c_id + ) + return res_list + elif isinstance(child_list, (str, dict, ParamRef, TemplateLoop)): + is_slot, slot_val = cls._resolve_slot(child_list, params) + if is_slot: + return slot_val + if isinstance(child_list, str): + return cls._map_id(child_list, instance_id) + return child_list + return child_list + + def _expand_static_layout( + self, + instance_id: InstanceId, + layout: Union[StaticTemplate, Template, Any], + passed_params: TemplateParams, + _depth: int = 0, + _call_stack: Optional[List[TemplateId]] = None, + ) -> A2UIComponentList: + template_id = getattr(layout, "template_id", "AnonymousTemplate") + + # 1. Resolve parameters and assign default values + params: Dict[str, Any] = {} + layout_params = getattr(layout, "parameters", {}) + for p_name, p_meta in layout_params.items(): + if isinstance(p_meta, Param): + p_meta_dict = p_meta.to_dict() + p_type = ( + p_meta.type.value + if isinstance(p_meta.type, ParamType) + else str(p_meta.type) + ) + else: + p_meta_dict = p_meta + p_type = str(p_meta.get("type", "string")) + + if p_name in passed_params: + val = passed_params[p_name] + if p_type in ["array", "children"]: + if isinstance(val, list): + params[p_name] = val + elif isinstance(val, dict): + params[p_name] = [val] + elif val == p_name or val is None: + params[p_name] = p_meta_dict.get("default", []) + else: + params[p_name] = [val] + else: + params[p_name] = val + + # Validate parameter value if data type + is_expression = isinstance(params[p_name], dict) and any( + k in params[p_name] for k in ["path", "concat", "format", "param"] + ) + if ( + isinstance(p_meta_dict, dict) + and p_type not in ["child", "children", "action"] + and not is_expression + and not ( + isinstance(params[p_name], str) + and ( + params[p_name].startswith("${") + or params[p_name].startswith("__") + ) + ) + ): + val_schema = normalize_a2ui_type_to_jsonschema(p_meta_dict) + if val_schema: + try: + jsonschema.validate( + instance=params[p_name], schema=val_schema + ) + except jsonschema.ValidationError as err: + raise ValueError( + f"Template '{template_id}': Parameter '{p_name}'" + f" failed validation: {err.message}" + ) from err + elif "default" in p_meta_dict and p_meta_dict["default"] is not None: + params[p_name] = p_meta_dict["default"] + elif p_type in ["array", "children"]: + params[p_name] = [] + elif p_meta_dict.get("required") is False or ( + isinstance(p_meta, Param) and not p_meta.required + ): + pass + else: + raise ValueError( + f"Missing required parameter '{p_name}' for template" + f" '{template_id}'" + ) + + # Merge in passed params that may not be in schema + for k, v in passed_params.items(): + if k not in params: + params[k] = v + + expanded_components: List[Dict[str, Any]] = [] + + # 2. First pass: Map internal component IDs, handle slots & loop unrolling + components_list = getattr(layout, "components", []) + for comp in components_list: + comp_copy = ( + comp.to_dict() if isinstance(comp, TemplateComponent) else dict(comp) + ) + comp_copy["id"] = self._map_id(comp_copy["id"], instance_id) + + if "child" in comp_copy: + is_slot, slot_val = self._resolve_slot(comp_copy["child"], params) + if is_slot: + if slot_val is None: + comp_copy.pop("child", None) + else: + comp_copy["child"] = slot_val + else: + if isinstance(comp_copy["child"], str): + comp_copy["child"] = self._map_id( + comp_copy["child"], instance_id + ) + + if "children" in comp_copy: + is_slot, slot_val = self._resolve_slot(comp_copy["children"], params) + if is_slot: + if slot_val is None: + comp_copy["children"] = [] + elif isinstance(slot_val, list): + comp_copy["children"] = slot_val + else: + comp_copy["children"] = [slot_val] + elif ( + isinstance(comp_copy["children"], dict) + and "param" in comp_copy["children"] + and ( + "template" in comp_copy["children"] + or "item" in comp_copy["children"] + ) + ): + children_meta = comp_copy["children"] + param_name = children_meta["param"] + array_data = params.get(param_name, []) + + if ( + isinstance(array_data, list) + and array_data + and all(isinstance(x, str) for x in array_data) + ): + comp_copy["children"] = array_data + else: + if not isinstance(array_data, list): + raise ValueError( + f"Template '{template_id}': Parameter '{param_name}'" + " must be an array/list for loop unrolling." + ) + + unrolled_child_ids = [] + + # Case A: Inline item layout loop + if "item" in children_meta and isinstance( + children_meta["item"], list + ): + item_comps = children_meta["item"] + as_var = children_meta.get("as") + for idx, item in enumerate(array_data): + sub_instance_id = f"{comp_copy['id']}_item_{idx}" + unrolled_child_ids.append(sub_instance_id) + + item_dict = ( + dict(item) + if isinstance(item, dict) + else {"value": item} + ) + sub_params = {**params, **item_dict} + if as_var: + sub_params[as_var] = item_dict + + inline_template = StaticTemplate( + template_id=f"{template_id}_inline_{idx}", + parameters={}, + components=item_comps, + ) + sub_expanded = self._expand_static_layout( + sub_instance_id, + inline_template, + sub_params, + _depth=_depth + 1, + _call_stack=_call_stack, + ) + expanded_components.extend(sub_expanded) + + # Case B: Named template loop + elif "template" in children_meta: + item_template_id = children_meta["template"] + for idx, item in enumerate(array_data): + sub_instance_id = f"{comp_copy['id']}_item_{idx}" + unrolled_child_ids.append(sub_instance_id) + sub_params = ( + item if isinstance(item, dict) else {"value": item} + ) + sub_expanded = self.expand_template( + sub_instance_id, + item_template_id, + sub_params, + _depth=_depth + 1, + _call_stack=_call_stack, + ) + expanded_components.extend(sub_expanded) + + comp_copy["children"] = unrolled_child_ids + else: + comp_copy["children"] = self._map_child_list( + comp_copy["children"], instance_id, params + ) + + comp_final = _substitute_params(comp_copy, params) + expanded_components.append(comp_final) + + # 4. Second pass: Flatten any nested template invocations + final_list: A2UIComponentList = [] + for c in expanded_components: + c_type = c.get("component") + if c_type in self.templates: + nested_instance_id = c["id"] + nested_template_id = c_type + nested_params = self._get_template_params( + c, self.templates[nested_template_id] + ) + nested_expanded = self.expand_template( + nested_instance_id, + nested_template_id, + nested_params, + _depth=_depth + 1, + _call_stack=_call_stack, + ) + final_list.extend(nested_expanded) + else: + final_list.append(c) + + return final_list + + def process_message( + self, message: Union[A2UIMessage, Sequence[A2UIMessage]] + ) -> Union[A2UIMessage, List[A2UIMessage]]: + """Intercepts A2UI server-to-client messages and unwraps any embedded template components.""" + if isinstance(message, list): + return [self.process_message(m) for m in message] + + if not isinstance(message, dict): + return message + + msg_copy = dict(message) + + for envelope_key in ["createSurface", "updateComponents"]: + if envelope_key in msg_copy: + payload = dict(msg_copy[envelope_key]) + if ( + envelope_key == "createSurface" + and "catalogId" in payload + and self.base_catalog_id is not None + ): + payload["catalogId"] = self.base_catalog_id + if "components" in payload: + components = payload["components"] + + # Normalize single top-level template instances to root ID + if len(components) == 1: + single_comp = components[0] + if ( + isinstance(single_comp, dict) + and single_comp.get("component") in self.templates + ): + fixed_comp = dict(single_comp) + fixed_comp["id"] = "root" + components = [fixed_comp] + + expanded_list: A2UIComponentList = [] + for comp in components: + comp_type = comp.get("component") + if comp_type in self.templates: + instance_id = comp.get("id", "root") + params = self._get_template_params( + comp, self.templates[comp_type] + ) + expanded = self.expand_template( + instance_id, comp_type, params + ) + expanded_list.extend(expanded) + else: + expanded_list.append(comp) + + payload["components"] = expanded_list + msg_copy[envelope_key] = payload + + return msg_copy + + def _promote_parameter(self, p_meta: Dict[str, Any]) -> JSONSchemaDict: + """Promotes a template parameter to an A2UI catalog schema property.""" + if not isinstance(p_meta, dict): + return p_meta + + common_types_url = ( + "https://a2ui.org/specification/v0_9/common_types.json" + if "0.9" in self.version + else "https://a2ui.org/specification/v1_0/common_types.json" + ) + + p_type = p_meta.get("type") + if p_type == "child": + res = {"$ref": f"{common_types_url}#/$defs/ComponentId"} + for k in ["title", "description", "default"]: + if k in p_meta: + res[k] = p_meta[k] + return res + if p_type == "children": + res = {"$ref": f"{common_types_url}#/$defs/ChildList"} + for k in ["title", "description", "default"]: + if k in p_meta: + res[k] = p_meta[k] + return res + if p_type == "action": + res = {"$ref": f"{common_types_url}#/$defs/Action"} + for k in ["title", "description", "default"]: + if k in p_meta: + res[k] = p_meta[k] + return res + if p_type == "enum" and "values" in p_meta: + res = { + "type": "string", + "enum": p_meta["values"], + } + for k in ["title", "description", "default"]: + if k in p_meta: + res[k] = p_meta[k] + return res + + ref_map = { + "string": f"{common_types_url}#/$defs/DynamicString", + "number": f"{common_types_url}#/$defs/DynamicNumber", + "integer": f"{common_types_url}#/$defs/DynamicNumber", + "boolean": f"{common_types_url}#/$defs/DynamicBoolean", + } + + target_ref = ref_map.get(str(p_type)) + if target_ref: + promoted: JSONSchemaDict = {"allOf": [{"$ref": target_ref}]} + metadata = {} + for k in ["title", "description", "default"]: + if k in p_meta: + metadata[k] = p_meta[k] + if metadata: + promoted["allOf"].append(metadata) + return promoted + + return dict(p_meta) + + def _validate_templates(self) -> None: + """Validates all registered templates.""" + for t_id, template in self.templates.items(): + if isinstance(template, StaticTemplate): + template.validate_definition() + for comp in template.components: + comp_dict = ( + comp.to_dict() if isinstance(comp, TemplateComponent) else comp + ) + comp_type = comp_dict.get("component") + if not comp_type: + raise ValueError( + f"Component in template '{t_id}' is missing 'component'" + " type." + ) + + def validate_templates(self) -> None: + """Deprecated alias for _validate_templates.""" + self._validate_templates() + + def _get_template_params( + self, + comp: A2UIComponent, + template: Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate], + ) -> Dict[str, Any]: + """Extracts passed parameters from a component dictionary.""" + params: Dict[str, Any] = {} + raw_keys = template.parameters.keys() if hasattr(template, "parameters") else [] + for p_name in raw_keys: + if p_name in comp: + params[p_name] = comp[p_name] + return params diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json new file mode 100644 index 0000000000..3c0eb0028f --- /dev/null +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json @@ -0,0 +1,273 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Template Definition", + "description": "Schema defining a parameterized A2UI declarative template authored in YAML with a nested layout tree and synthetic ID generation.", + "type": "object", + "required": ["version", "templateId", "parameters"], + "properties": { + "version": { + "type": "string", + "const": "0.1", + "description": "Specification version of the template definition format. Forced to '0.1'." + }, + "templateId": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_]*$", + "description": "Unique identifier and component tag name of the template." + }, + "description": { + "type": "string", + "description": "Human-readable description of what this template represents." + }, + "parameters": { + "type": "object", + "description": "Mapping of parameter names to their explicit A2UI type definitions.", + "additionalProperties": { + "$ref": "#/$defs/ParameterDefinition" + } + }, + "requiredParameters": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional list of parameter names that are required when invoking the template." + }, + "layout": { + "$ref": "#/$defs/NestedTemplateNode", + "description": "Root component of the nested layout tree." + }, + "components": { + "type": "array", + "description": "Optional flattened component list for internal graph representation.", + "items": { + "$ref": "#/$defs/TemplateComponent" + } + }, + "sampleData": { + "type": "object", + "description": "Sample parameter values used for previewing and testing template inflation." + } + }, + "anyOf": [ + {"required": ["layout"]}, + {"required": ["components"]} + ], + "additionalProperties": false, + "$defs": { + "ParameterDefinition": { + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "enum": [ + "string", + "number", + "integer", + "boolean", + "enum", + "object", + "array", + "child", + "children", + "action" + ], + "description": "A2UI semantic parameter type." + }, + "title": { + "type": "string", + "description": "Short human-readable title of the parameter." + }, + "description": { + "type": "string", + "description": "Detailed explanation of the parameter's purpose and usage." + }, + "default": { + "description": "Default fallback value if not specified." + }, + "values": { + "type": "array", + "items": {"type": "string"}, + "description": "Allowed string values when type is 'enum'." + }, + "items": { + "description": "Item type definition when type is 'array'. Can be an A2UI type name or nested ParameterDefinition." + }, + "properties": { + "type": "object", + "description": "Property definitions when type is 'object'.", + "additionalProperties": { + "$ref": "#/$defs/ParameterDefinition" + } + }, + "required": { + "anyOf": [ + { + "type": "boolean", + "description": "Whether this parameter is required when instantiating the template." + }, + { + "type": "array", + "items": {"type": "string"}, + "description": "List of required property names for object types." + } + ] + }, + "minimum": { + "type": "number", + "description": "Minimum value constraint for numeric parameters." + }, + "maximum": { + "type": "number", + "description": "Maximum value constraint for numeric parameters." + } + }, + "additionalProperties": false + }, + "ParamReference": { + "type": "object", + "required": ["param"], + "properties": { + "param": { + "type": "string", + "description": "Name or dot-separated path of the template parameter to substitute." + }, + "template": { + "type": "string", + "description": "Child template identifier when mapping an array parameter to child component instances." + }, + "default": { + "description": "Fallback value if the parameter path resolves to None." + } + }, + "additionalProperties": false + }, + "ConcatExpression": { + "type": "object", + "required": ["concat"], + "properties": { + "concat": { + "type": "array", + "description": "List of literal values and parameter references to concatenate into a string.", + "items": { + "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] + } + } + }, + "additionalProperties": false + }, + "FormatExpression": { + "type": "object", + "required": ["format"], + "properties": { + "format": { + "type": "string", + "description": "Format string containing {key} placeholders." + }, + "args": { + "type": "object", + "description": "Mapping of placeholder keys to values or parameter references.", + "additionalProperties": { + "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] + } + } + }, + "additionalProperties": false + }, + "LoopDefinition": { + "type": "object", + "required": ["param"], + "anyOf": [ + {"required": ["template"]}, + {"required": ["item"]} + ], + "properties": { + "param": { + "type": "string", + "description": "Parameter name holding the list/array data to iterate over." + }, + "template": { + "type": "string", + "description": "Identifier of a named template to instantiate for each item." + }, + "item": { + "$ref": "#/$defs/NestedTemplateNode", + "description": "Self-contained inline component layout to instantiate for each item." + }, + "as": { + "type": "string", + "description": "Optional variable scope alias for accessing item fields in expressions." + } + }, + "additionalProperties": false + }, + "TemplateValue": { + "description": "A literal value, parameter substitution, or expression in a template component property.", + "anyOf": [ + {"type": "string"}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "null"}, + {"$ref": "#/$defs/ParamReference"}, + {"$ref": "#/$defs/ConcatExpression"}, + {"$ref": "#/$defs/FormatExpression"}, + {"type": "array"}, + {"type": "object"} + ] + }, + "NestedTemplateNode": { + "type": "object", + "required": ["component"], + "properties": { + "id": { + "type": "string", + "description": "Optional explicit component ID within the template." + }, + "component": { + "type": "string", + "description": "Catalog component tag name." + }, + "child": { + "anyOf": [ + {"$ref": "#/$defs/NestedTemplateNode"}, + {"$ref": "#/$defs/TemplateValue"} + ] + }, + "children": { + "anyOf": [ + { + "type": "object", + "required": ["loop"], + "properties": { + "loop": {"$ref": "#/$defs/LoopDefinition"} + }, + "additionalProperties": false + }, + {"type": "array"}, + {"$ref": "#/$defs/ParamReference"}, + {"type": "string"} + ] + } + }, + "additionalProperties": { + "$ref": "#/$defs/TemplateValue" + } + }, + "TemplateComponent": { + "type": "object", + "required": ["id", "component"], + "properties": { + "id": { + "type": "string", + "description": "Unique component ID within the template." + }, + "component": { + "type": "string", + "description": "Catalog component tag name." + } + }, + "additionalProperties": { + "$ref": "#/$defs/TemplateValue" + } + } + } +} diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/template/__init__.py b/agent_sdks/python/a2ui_agent/src/a2ui/template/__init__.py deleted file mode 100644 index 10c96753bb..0000000000 --- a/agent_sdks/python/a2ui_agent/src/a2ui/template/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py new file mode 100644 index 0000000000..619dfc97f3 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py @@ -0,0 +1,952 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from pathlib import Path +import pytest +from a2ui.inference_formats.experimental.template import ( + Template, + StaticTemplate, + DynamicTemplate, + Param, + ParamType, + ParamRef, + Concat, + TemplateComponent, + TemplateLoop, + TemplateProcessor, + TemplateInferenceFormat, + A2uiTemplateManager, + flatten_nested_layout, + normalize_node, + A2UIComponent, + A2UIComponentList, + TemplateParams, + TemplateId, + InstanceId, +) + + +def get_examples_dir() -> str: + curr = Path(__file__).resolve() + while curr != curr.parent: + if (curr / "specification").exists(): + return str(curr / "specification" / "proposals" / "templates" / "examples") + curr = curr.parent + raise RuntimeError("Could not locate repo root containing 'specification'") + + +def load_example(name: str) -> Template: + file_path = os.path.join(get_examples_dir(), f"{name}.yaml") + return Template.from_yaml_file(file_path)[0] + + +def test_user_profile_template_expansion(): + user_profile = load_example("user_profile") + processor = TemplateProcessor(templates=[user_profile]) + + expanded = processor.expand_template( + instance_id="alice_card", + template_id="UserProfile", + passed_params={ + "userId": "u123", + "userName": "Alice Smith", + "role": "Admin", + }, + ) + + assert len(expanded) == 5 + + card = next(c for c in expanded if c["id"] == "alice_card") + assert card["component"] == "Card" + + name_text = next( + c + for c in expanded + if "text" in c and c.get("component") == "Text" and c["text"] == "Alice Smith" + ) + assert name_text is not None + + role_text = next( + c + for c in expanded + if "text" in c and c.get("component") == "Text" and c["text"] == "Admin" + ) + assert role_text is not None + + +def test_user_profile_default_role(): + user_profile = load_example("user_profile") + processor = TemplateProcessor(templates=[user_profile]) + + expanded = processor.expand_template( + instance_id="bob_card", + template_id="UserProfile", + passed_params={ + "userId": "u456", + "userName": "Bob", + }, + ) + + role_text = next(c for c in expanded if "text" in c and c["text"] == "Member") + assert role_text is not None + + +def test_nested_team_card_loop_unrolling(): + user_profile = load_example("user_profile") + team_card = load_example("team_card") + processor = TemplateProcessor(templates=[user_profile, team_card]) + + data = { + "teamName": "Core Architecture", + "members": [ + {"userId": "u1", "userName": "Elena", "role": "Principal"}, + {"userId": "u2", "userName": "Marcus"}, + ], + } + + expanded = processor.expand_template( + instance_id="arch_team", + template_id="TeamCard", + passed_params=data, + ) + + root_card = next(c for c in expanded if c["id"] == "arch_team") + assert root_card["component"] == "Card" + + # Elena's name + elena_name = next(c for c in expanded if "text" in c and c["text"] == "Elena") + assert elena_name is not None + + # Marcus default role + marcus_role = next(c for c in expanded if "text" in c and c["text"] == "Member") + assert marcus_role is not None + + +def test_team_roster_template(): + user_profile = load_example("user_profile") + team_card = load_example("team_card") + team_roster = load_example("team_roster") + + processor = TemplateProcessor(templates=[user_profile, team_card, team_roster]) + + expanded = processor.expand_template( + instance_id="roster", + template_id="TeamRoster", + passed_params={ + "directoryTitle": "Global Directory", + "children": ["team_1", "team_2"], + }, + ) + + root = next(c for c in expanded if c["id"] == "roster") + assert root["component"] == "Column" + + +def test_goal_item_and_team_goal_list(): + goal_item = load_example("goal_item") + team_goal_list = load_example("team_goal_list") + + processor = TemplateProcessor(templates=[goal_item, team_goal_list]) + + expanded = processor.expand_template( + instance_id="team_goals", + template_id="TeamGoalList", + passed_params={ + "teamName": "Protocol Team", + "goals": [ + { + "title": "Upgrade Agent SDK", + "priority": "High", + "targetDate": "2026-08-30", + }, + {"title": "Clean Demos", "targetDate": "2026-09-15"}, + ], + }, + ) + + assert any(c["id"] == "team_goals" for c in expanded) + assert any("Upgrade Agent SDK" in str(c.get("text")) for c in expanded) + + +def test_feedback_item_and_board(): + feedback_item = load_example("feedback_item") + feedback_board = load_example("team_feedback_board") + + processor = TemplateProcessor(templates=[feedback_item, feedback_board]) + + expanded = processor.expand_template( + instance_id="board", + template_id="TeamFeedbackBoard", + passed_params={ + "teamName": "Core Engineers", + "feedbacks": [ + {"author": "Alice", "note": "Great synchronous speed!", "rating": 5}, + ], + }, + ) + + assert any(c["id"] == "board" for c in expanded) + assert any("Great synchronous speed!" in str(c.get("text")) for c in expanded) + + +def test_section_card_with_slots(): + section_card = load_example("section_card") + processor = TemplateProcessor(templates=[section_card]) + + expanded = processor.expand_template( + instance_id="sec", + template_id="SectionCard", + passed_params={ + "title": "Settings", + "description": "Configure your options", + "headerAction": "settings_btn", + "children": ["opt_1", "opt_2"], + }, + ) + + action_slot = next(c for c in expanded if c["id"] == "sec_action_slot") + assert action_slot["children"] == ["settings_btn"] + + body_container = next(c for c in expanded if c["id"] == "sec_body_container") + assert body_container["children"] == ["opt_1", "opt_2"] + + +def test_process_message_create_surface(): + user_profile = load_example("user_profile") + processor = TemplateProcessor(templates=[user_profile]) + + msg = { + "version": "v0.9", + "createSurface": { + "surfaceId": "user_view", + "components": [{ + "id": "my_user", + "component": "UserProfile", + "userId": "usr_999", + "userName": "Sarah Connor", + "role": "Defender", + }], + }, + } + + processed = processor.process_message(msg) + components = processed["createSurface"]["components"] + assert len(components) == 5 + assert components[0]["id"] == "root" + assert components[0]["component"] == "Card" + + +def test_template_inference_format_end_to_end(): + user_profile = load_example("user_profile") + manager = TemplateInferenceFormat( + templates=[user_profile], + surface_id="main_surface", + version="0.9.1", + ) + + # 1. Check prompt generation includes UserProfile signature when include_schema=True + prompt = manager.prompt_generator.generate( + role_description="Test Agent", include_schema=True + ) + assert "UserProfile" in prompt + + # 2. Check parsing and expansion of Express DSL response + llm_output = """ + Here is your profile card: + + root = UserProfile("usr_99", "Sarah Jenkins", "Lead Designer") + + """ + + parts = manager.parser.parse_response(llm_output) + assert len(parts) == 1 + assert "Here is your profile card:" in parts[0].text + assert parts[0].a2ui_json is not None + + messages = parts[0].a2ui_json + assert len(messages) == 2 + + # In v0.9.1, createSurface and updateComponents are separate + create_surface = messages[0].get("createSurface") + assert create_surface is not None + assert create_surface["surfaceId"] == "main_surface" + + update_components = messages[1].get("updateComponents") + assert update_components is not None + assert update_components["surfaceId"] == "main_surface" + + components = update_components["components"] + assert len(components) == 5 + name_text = next( + c + for c in components + if c.get("component") == "Text" and c.get("text") == "Sarah Jenkins" + ) + assert name_text is not None + role_text = next( + c + for c in components + if c.get("component") == "Text" and c.get("text") == "Lead Designer" + ) + assert role_text is not None + + +def test_multi_document_yaml_loading_and_forward_references(): + yaml_suite = """ +templateId: RosterView +description: Top-level roster view. +parameters: + title: {type: string} + members: {type: array} +layout: + component: Column + children: + - component: Text + text: ${title} + variant: h1 + - component: Column + children: + loop: + param: members + template: MemberCard # Forward reference to MemberCard below! + +--- + +templateId: MemberCard +description: Member identity card. +parameters: + name: {type: string} + title: {type: string} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${name} + variant: h3 + - component: Text + text: ${title} + variant: caption +""" + + templates = StaticTemplate.from_yaml_string(yaml_suite) + assert len(templates) == 2 + assert templates[0].template_id == "RosterView" + assert templates[1].template_id == "MemberCard" + + # Expand RosterView (which forward-references MemberCard) + processor = TemplateProcessor(templates=templates) + expanded = processor.expand_template( + "inst_1", + "RosterView", + { + "title": "Engineering Team", + "members": [ + {"name": "Alice", "title": "Staff Engineer"}, + {"name": "Bob", "title": "Tech Lead"}, + ], + }, + ) + + assert any(c.get("text") == "Engineering Team" for c in expanded) + assert any(c.get("text") == "Alice" for c in expanded) + assert any(c.get("text") == "Bob" for c in expanded) + + +def test_inline_loop_expansion(): + yaml_tmpl = """ +templateId: InlineListCard +description: Card with inline unrolled loop items. +parameters: + teamName: {type: string} + skills: {type: array} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${teamName} + variant: h2 + - component: Column + children: + loop: + param: skills + as: skill + item: + component: Row + justify: spaceBetween + children: + - component: Text + text: ${skill.name} + - component: Text + text: "Lvl ${skill.level}" +""" + + template = StaticTemplate.from_yaml(yaml_tmpl) + processor = TemplateProcessor(templates=[template]) + + expanded = processor.expand_template( + "inst_inline", + "InlineListCard", + { + "teamName": "Frontend Guild", + "skills": [ + {"name": "TypeScript", "level": "Expert"}, + {"name": "A2UI Protocols", "level": "Master"}, + ], + }, + ) + + assert any(c.get("text") == "Frontend Guild" for c in expanded) + assert any(c.get("text") == "TypeScript" for c in expanded) + assert any(c.get("text") == "Lvl Master" for c in expanded) + + +def test_missing_template_reference_raises_clear_error(): + yaml_tmpl = """ +templateId: BadReference +parameters: + items: {type: array} +layout: + component: Column + children: + loop: + param: items + template: NonExistentTemplate +""" + tmpl = StaticTemplate.from_yaml(yaml_tmpl) + processor = TemplateProcessor(templates=[tmpl]) + + with pytest.raises( + ValueError, match="Template 'NonExistentTemplate' is not registered" + ): + processor.expand_template("inst_bad", "BadReference", {"items": [{"id": 1}]}) + + +def test_recursion_cycle_guard(): + # Template A references Template B, and Template B references Template A + yaml_suite = """ +templateId: NodeA +parameters: + dummy: {type: string, default: "x"} +layout: + component: Column + children: + - component: NodeB + +--- + +templateId: NodeB +parameters: + dummy: {type: string, default: "y"} +layout: + component: Column + children: + - component: NodeA +""" + templates = StaticTemplate.from_yaml_string(yaml_suite) + processor = TemplateProcessor(templates=templates) + + with pytest.raises(ValueError, match="Circular template reference detected"): + processor.expand_template("cycle_root", "NodeA", {}) + + +def test_non_text_parameter_type_preservation(): + yaml_tmpl = """ +templateId: TypedControls +description: Card showing numbers, booleans, and objects. +parameters: + count: {type: integer} + isActive: {type: boolean} + actionObj: {type: action} +layout: + component: Card + child: + component: Column + children: + - component: Slider + value: ${count} + disabled: ${isActive} + - component: Button + onClick: ${actionObj} +""" + + tmpl = StaticTemplate.from_yaml(yaml_tmpl) + processor = TemplateProcessor(templates=[tmpl]) + + expanded = processor.expand_template( + "ctrl_1", + "TypedControls", + { + "count": 42, + "isActive": False, + "actionObj": {"actionId": "submit_form", "context": {"user": "alice"}}, + }, + ) + + slider = next(c for c in expanded if c.get("component") == "Slider") + assert slider["value"] == 42 + assert type(slider["value"]) is int + assert slider["disabled"] is False + assert type(slider["disabled"]) is bool + + btn = next(c for c in expanded if c.get("component") == "Button") + assert btn["onClick"] == {"actionId": "submit_form", "context": {"user": "alice"}} + assert type(btn["onClick"]) is dict + + +def test_dynamic_template_with_resolver(): + salary_layout = load_example("salary_card") + + # Mock database lookup resolver + def resolve_salary(employeeId: str): + db = { + "emp_101": { + "employeeName": "Dr. Elena Vance", + "role": "Principal Systems Architect", + "baseSalary": "$215,000", + "annualBonus": "$45,000", + "equity": "3,500 RSUs", + "clearanceLevel": "Level 5 - Confidential", + "verifiedAt": "2026-08-13", + } + } + if employeeId not in db: + raise ValueError(f"Unknown employee {employeeId}") + return db[employeeId] + + dynamic_tmpl = DynamicTemplate( + template_id="EmployeeSalaryCard", + resolver=resolve_salary, + layout=salary_layout, + description="Secure employee salary card resolved server-side.", + ) + + assert "employeeId" in dynamic_tmpl.parameters + assert dynamic_tmpl.is_dynamic is True + + processor = TemplateProcessor(templates=[dynamic_tmpl]) + synthetic_cat = processor.generate_inference_catalog() + assert "EmployeeSalaryCard" in synthetic_cat["components"] + + expanded = processor.expand_template( + "salary_inst", "EmployeeSalaryCard", {"employeeId": "emp_101"} + ) + assert len(expanded) > 5 + + name_txt = next(c for c in expanded if c.get("text") == "Dr. Elena Vance") + assert name_txt is not None + + sal_val = next(c for c in expanded if c.get("text") == "$215,000") + assert sal_val is not None + + +def test_programmatic_dynamic_template_render(): + def render_server_health(serverId: str, includeDisks: bool = True): + status_icon = "check_circle" if serverId == "srv_01" else "warning" + + disk_items = [] + if includeDisks: + mounts = [("/", 45), ("/data", 82), ("/var/log", 12)] + for mount, usage in mounts: + disk_items.append({ + "component": "Row", + "justify": "spaceBetween", + "children": [ + {"component": "Text", "text": mount, "variant": "caption"}, + { + "component": "Text", + "text": f"{usage}% used", + "variant": "caption", + }, + ], + }) + + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + { + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": [ + { + "component": "Text", + "text": f"Server {serverId}", + "variant": "h3", + }, + {"component": "Icon", "name": status_icon}, + ], + }, + {"component": "Divider", "axis": "horizontal"}, + {"component": "Text", "text": "CPU Load: 24.5%", "variant": "body"}, + {"component": "Column", "children": disk_items}, + ], + }, + } + + dyn_tmpl = DynamicTemplate( + template_id="LiveServerHealth", + render=render_server_health, + description="Live server diagnostics.", + ) + + assert "serverId" in dyn_tmpl.parameters + assert "includeDisks" in dyn_tmpl.parameters + + processor = TemplateProcessor(templates=[dyn_tmpl]) + expanded = processor.expand_template( + "srv_dash", + "LiveServerHealth", + {"serverId": "srv_01", "includeDisks": True}, + ) + + assert len(expanded) > 6 + root_card = next(c for c in expanded if c["id"] == "srv_dash") + assert root_card["component"] == "Card" + + server_title = next(c for c in expanded if c.get("text") == "Server srv_01") + assert server_title is not None + + disk_entry = next(c for c in expanded if c.get("text") == "82% used") + assert disk_entry is not None + + +def test_programmatic_dynamic_template_duck_typing(): + # Mock class that implements .to_dict() protocol + class MockCustomBadge: + + def __init__(self, label: str): + self.label = label + + def to_dict(self): + return { + "component": "Card", + "child": { + "component": "Text", + "text": f"[ {self.label} ]", + "variant": "caption", + }, + } + + def render_badge(tag: str): + return MockCustomBadge(tag) + + dyn_tmpl = DynamicTemplate( + template_id="CustomBadgeView", + render=render_badge, + ) + + processor = TemplateProcessor(templates=[dyn_tmpl]) + expanded = processor.expand_template( + "badge_inst", "CustomBadgeView", {"tag": "ACTIVE"} + ) + + root = next(c for c in expanded if c["id"] == "badge_inst") + assert root["component"] == "Card" + text_node = next(c for c in expanded if c.get("text") == "[ ACTIVE ]") + assert text_node is not None + + +def test_template_versioning_valid(): + yaml_content = """ +version: "0.1" +templateId: SimpleVersionedTest +parameters: + title: + type: string +layout: + component: Text + text: ${title} +""" + tmpl = StaticTemplate.from_yaml(yaml_content) + assert tmpl.version == "0.1" + d = tmpl.to_dict() + assert d["version"] == "0.1" + assert "version: '0.1'" in tmpl.to_yaml() or 'version: "0.1"' in tmpl.to_yaml() + + +def test_template_versioning_invalid(): + yaml_content = """ +version: "0.2" +templateId: InvalidVersionTest +parameters: + title: + type: string +layout: + component: Text + text: ${title} +""" + with pytest.raises(ValueError, match="Unsupported template version"): + StaticTemplate.from_yaml(yaml_content) + + +def test_dynamic_template_resolve_non_dict_fallback(): + # Resolver returns None instead of a dict + def bad_resolver(): + return None + + layout = StaticTemplate.from_yaml(""" +version: "0.1" +templateId: LayoutForBadResolver +parameters: + name: + type: string + default: FallbackName +layout: + component: Text + text: ${name} +""") + + dyn_tmpl = DynamicTemplate( + template_id="BadResolverTemplate", + resolver=bad_resolver, + layout=layout, + ) + processor = TemplateProcessor(templates=[dyn_tmpl]) + # Should not raise TypeError: unsupported operand type(s) for **: 'NoneType' + expanded = processor.expand_template("bad_inst", "BadResolverTemplate", {}) + assert any(c.get("text") == "FallbackName" for c in expanded) + + +def test_template_data_binding_expression_on_primitive_param(): + yaml_content = """ +version: "0.1" +templateId: PrimitiveWithBinding +parameters: + label: + type: string +layout: + component: Text + text: ${label} +""" + tmpl = StaticTemplate.from_yaml(yaml_content) + processor = TemplateProcessor(templates=[tmpl]) + # Pass an A2UI DataBinding dict to a primitive string param + expanded = processor.expand_template( + "inst_1", "PrimitiveWithBinding", {"label": {"path": "/session/userName"}} + ) + text_node = next(c for c in expanded if c["id"] == "inst_1") + assert text_node["text"] == {"path": "/session/userName"} + + +def test_programmatic_dynamic_template_extra_params_filtered(): + # Function that takes only 'title' without **kwargs + def strict_render(title: str): + return { + "component": "Text", + "text": title, + } + + dyn_tmpl = DynamicTemplate( + template_id="StrictTitleView", + render=strict_render, + ) + processor = TemplateProcessor(templates=[dyn_tmpl]) + # Pass extra parameter 'extra_field' that strict_render doesn't accept + expanded = processor.expand_template( + "strict_inst", + "StrictTitleView", + {"title": "Hello World", "extra_field": 12345}, + ) + text_node = next(c for c in expanded if c["id"] == "strict_inst") + assert text_node["text"] == "Hello World" + + +def test_loop_definition_anyof_validation(): + # Loop missing both 'template' and 'item' + invalid_yaml = """ +version: "0.1" +templateId: InvalidLoopTemplate +parameters: + items: + type: array +layout: + component: Column + children: + loop: + param: items +""" + with pytest.raises(ValueError, match="fails template_definition.json schema"): + StaticTemplate.from_yaml(invalid_yaml) + + +def test_circular_template_reference_detection(): + # Template A references Template B which references Template A + tmpl_a = StaticTemplate.from_yaml(""" +version: "0.1" +templateId: CircularA +parameters: {} +layout: + component: CircularB +""") + + tmpl_b = StaticTemplate.from_yaml(""" +version: "0.1" +templateId: CircularB +parameters: {} +layout: + component: CircularA +""") + + processor = TemplateProcessor(templates=[tmpl_a, tmpl_b]) + with pytest.raises(ValueError, match="Circular template reference detected"): + processor.expand_template("inst_circ", "CircularA", {}) + + +def test_normalize_node_dataclass_support(): + from dataclasses import dataclass + + @dataclass + class CustomComponent: + component: str + text: str + + node = CustomComponent(component="Text", text="Hello Dataclass") + d = normalize_node(node) + assert isinstance(d, dict) + assert d["component"] == "Text" + assert d["text"] == "Hello Dataclass" + + +def test_dynamic_template_loop_custom_as_variable(): + # Verify that custom 'as: member' works without failing validation + custom_loop_yaml = """ +version: "0.1" +templateId: CustomLoopVarTemplate +parameters: + members: + type: array +layout: + component: Column + children: + loop: + param: members + as: member + item: + component: Text + text: ${member.name} +""" + tmpl = StaticTemplate.from_yaml(custom_loop_yaml) + assert tmpl.template_id == "CustomLoopVarTemplate" + + +def test_template_processor_completely_optional_base_catalog(): + tmpl = StaticTemplate.from_yaml(""" +version: "0.1" +templateId: PureTemplate +parameters: + title: {type: string} +layout: + component: Card + child: + component: Text + text: ${title} +""") + # Initialize processor with NO base catalog whatsoever + processor = TemplateProcessor(templates=[tmpl]) + assert processor.base_catalog is None + assert processor.base_catalog_id is None + + # Synthetic catalog has no hardcoded primitives, only registered template + cat = processor.generate_inference_catalog() + assert "PureTemplate" in cat["components"] + assert "Card" not in cat["components"] + assert "Text" not in cat["components"] + + # Expansion works flawlessly + expanded = processor.expand_template( + "pure_inst", "PureTemplate", {"title": "Autonomous"} + ) + assert len(expanded) == 2 + assert any(c.get("text") == "Autonomous" for c in expanded) + + # Process message does not inject any catalogId if base_catalog_id is None + msg = { + "createSurface": { + "surfaceId": "s1", + "catalogId": "client_custom_catalog", + "components": [], + } + } + out_msg = processor.process_message(msg) + assert out_msg["createSurface"]["catalogId"] == "client_custom_catalog" + + +def test_template_processor_with_custom_domain_catalog(): + # Catalog from custom domain with zero basic catalog references + custom_catalog = { + "$id": "https://company.org/custom_medical_catalog.json", + "components": { + "PatientChart": {"type": "object"}, + "VitalsGauge": {"type": "object"}, + }, + "functions": {}, + } + + tmpl = StaticTemplate.from_yaml(""" +version: "0.1" +templateId: MedicalReport +parameters: + patientName: {type: string} +layout: + component: PatientChart + properties: + name: ${patientName} +""") + + processor = TemplateProcessor(templates=[tmpl], base_catalog=custom_catalog) + assert ( + processor.base_catalog_id == "https://company.org/custom_medical_catalog.json" + ) + + # Inference catalog dynamically discovers custom catalog components + cat = processor.generate_inference_catalog() + assert "PatientChart" in cat["components"] + assert "VitalsGauge" in cat["components"] + assert "MedicalReport" in cat["components"] + # No standard Basic Catalog primitives injected + assert "Card" not in cat["components"] + assert "Button" not in cat["components"] + + # Process message properly rewrites surface catalogId to custom catalog ID + msg = { + "createSurface": { + "surfaceId": "med_surf", + "catalogId": "placeholder", + "components": [], + } + } + out_msg = processor.process_message(msg) + assert ( + out_msg["createSurface"]["catalogId"] + == "https://company.org/custom_medical_catalog.json" + ) + + +def test_type_aliases_and_private_helpers(): + params: TemplateParams = {"user": {"profile": {"tier": "Pro"}}} + from a2ui.inference_formats.experimental.template.processor import _resolve_param_path, _substitute_params + + found, val = _resolve_param_path("user.profile.tier", params) + assert found is True + assert val == "Pro" + + sub = _substitute_params("${user.profile.tier}", params) + assert sub == "Pro" diff --git a/uv.lock b/uv.lock index eda28872f6..2e95d522fd 100644 --- a/uv.lock +++ b/uv.lock @@ -68,6 +68,8 @@ dependencies = [ { name = "google-genai" }, { name = "httpx" }, { name = "jsonschema" }, + { name = "nest-asyncio" }, + { name = "pyyaml" }, ] [package.dev-dependencies] @@ -75,6 +77,7 @@ dev = [ { name = "antlr4-tools" }, { name = "hatchling" }, { name = "types-jsonschema" }, + { name = "types-pyyaml" }, ] [package.metadata] @@ -86,6 +89,8 @@ requires-dist = [ { name = "google-genai", specifier = ">=1.27.0" }, { name = "httpx", specifier = ">=0.27.0" }, { name = "jsonschema", specifier = ">=4.0.0" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "pyyaml", specifier = ">=6.0" }, ] [package.metadata.requires-dev] @@ -93,6 +98,7 @@ dev = [ { name = "antlr4-tools", specifier = ">=0.2.1" }, { name = "hatchling", specifier = ">=1.30.1" }, { name = "types-jsonschema" }, + { name = "types-pyyaml" }, ] [[package]] @@ -3048,6 +3054,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + [[package]] name = "nest-asyncio2" version = "1.7.2" From 1147970ae319b234e54cc7f55fe740638cf0bcb1 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 14:39:09 +0930 Subject: [PATCH 07/26] test(templates): add declarative conformance test suite and boundary test coverage --- .../experimental/template/models.py | 22 +- .../experimental/template/processor.py | 149 +++++++------- .../template/conformance/README.md | 63 ++++++ .../schema/template_conformance_schema.json | 78 +++++++ .../conformance/suites/01_substitution.yaml | 151 ++++++++++++++ .../suites/02_static_expansion.yaml | 192 ++++++++++++++++++ .../conformance/suites/03_loop_unrolling.yaml | 176 ++++++++++++++++ .../suites/04_parameter_validation.yaml | 89 ++++++++ .../suites/05_message_interception.yaml | 144 +++++++++++++ .../suites/06_error_invariants.yaml | 47 +++++ .../template/test_template_conformance.py | 156 ++++++++++++++ .../template/test_template_format.py | 8 +- 12 files changed, 1193 insertions(+), 82 deletions(-) create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/README.md create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py index 22b45f5b76..70c532ffc5 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -384,10 +384,14 @@ def flatten_nested_layout( node_id = str(node_copy["id"]) elif parent_id is None: node_id = "root" - elif index is not None: + elif slot_name and index is not None: node_id = f"{parent_id}_{slot_name}_{index}_{comp_type}" - else: + elif slot_name: node_id = f"{parent_id}_{slot_name}_{comp_type}" + elif index is not None: + node_id = f"{parent_id}_{index}_{comp_type}" + else: + node_id = f"{parent_id}_{comp_type}" node_copy["id"] = node_id flat_components: List[Dict[str, Any]] = [] @@ -424,9 +428,7 @@ def flatten_nested_layout( if "as" in loop_cfg: loop_dict["as"] = loop_cfg["as"] if "item" in loop_cfg and isinstance(loop_cfg["item"], dict): - _, item_sub_comps = flatten_nested_layout( - loop_cfg["item"], parent_id=f"{node_id}_item" - ) + _, item_sub_comps = flatten_nested_layout(loop_cfg["item"]) loop_dict["item"] = item_sub_comps elif "template" in loop_cfg: loop_dict["template"] = loop_cfg["template"] @@ -440,9 +442,7 @@ def flatten_nested_layout( if "as" in loop_cfg: loop_dict["as"] = loop_cfg["as"] if "item" in loop_cfg and isinstance(loop_cfg["item"], dict): - _, item_sub_comps = flatten_nested_layout( - loop_cfg["item"], parent_id=f"{node_id}_item" - ) + _, item_sub_comps = flatten_nested_layout(loop_cfg["item"]) loop_dict["item"] = item_sub_comps elif "template" in loop_cfg: loop_dict["template"] = loop_cfg["template"] @@ -512,7 +512,11 @@ def from_dict(cls, data: Dict[str, Any]) -> StaticTemplate: if not template_id: raise ValueError("Template dictionary must contain 'templateId'.") - version = data.get("version", "0.1") + version = data.get("version") + if not version: + raise ValueError( + f"Template '{template_id}' missing required 'version' attribute." + ) if version != "0.1": raise ValueError( f"Unsupported template version '{version}'. Currently only '0.1' is" diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py index 337c49e558..172dd37e22 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py @@ -150,7 +150,7 @@ def _substitute_params(val: Any, params: TemplateParams) -> Any: return [_substitute_params(x, params) for x in val] elif isinstance(val, str): - # Exact parameter match preserving type: e.g. "${user}" or "${score}" + # 1. Exact parameter match preserving type: e.g. "${user}" or "${score}" match = re.match(r"^\$\{([\w\.]+)\}$", val) if match: param_path = match.group(1) @@ -158,9 +158,13 @@ def _substitute_params(val: Any, params: TemplateParams) -> Any: if found: return res - # In-string placeholder replacement: replace all occurrences of ${path} - pattern = re.compile(r"\$\{([\w\.]+)\}") + # 2. Check for exact escaped token match: e.g. r"\${user}" -> "${user}" + escaped_match = re.match(r"^\\(\$\{[\w\.]+\})$", val) + if escaped_match: + return escaped_match.group(1) + # 3. In-string replacement with escape handling: + # Negative lookbehind (? str: path = m.group(1) found, res = _resolve_param_path(path, params) @@ -168,7 +172,11 @@ def replacer(m: re.Match[str]) -> str: return str(res) return str(m.group(0)) - return str(pattern.sub(replacer, val)) + # Replace unescaped tokens + substituted = re.sub(r"(? "${path}" + final_str = re.sub(r"\\(\$\{[\w\.]+\})", r"\1", substituted) + return final_str return val @@ -481,6 +489,8 @@ def expand_template( def _map_id(internal_id: str, instance_id: InstanceId) -> ComponentId: if internal_id == "root": return instance_id + if internal_id.startswith("root_"): + return f"{instance_id}_{internal_id[5:]}" return f"{instance_id}_{internal_id}" @staticmethod @@ -505,11 +515,13 @@ def _resolve_slot(val: Any, params: TemplateParams) -> Tuple[bool, Any]: found, res = _resolve_param_path(p_path, params) if found: return True, res + return True, None elif val.startswith("__PARAM__"): p_name = val[9:] found, res = _resolve_param_path(p_name, params) if found: return True, res + return True, None return False, val @classmethod @@ -568,10 +580,10 @@ def _expand_static_layout( if p_name in passed_params: val = passed_params[p_name] - if p_type in ["array", "children"]: + if p_type == "children": if isinstance(val, list): params[p_name] = val - elif isinstance(val, dict): + elif isinstance(val, (dict, str)): params[p_name] = [val] elif val == p_name or val is None: params[p_name] = p_meta_dict.get("default", []) @@ -670,73 +682,66 @@ def _expand_static_layout( param_name = children_meta["param"] array_data = params.get(param_name, []) - if ( - isinstance(array_data, list) - and array_data - and all(isinstance(x, str) for x in array_data) - ): - comp_copy["children"] = array_data - else: - if not isinstance(array_data, list): - raise ValueError( - f"Template '{template_id}': Parameter '{param_name}'" - " must be an array/list for loop unrolling." - ) + if not isinstance(array_data, list): + raise ValueError( + f"Template '{template_id}': Parameter '{param_name}'" + " must be an array/list for loop unrolling." + ) - unrolled_child_ids = [] + unrolled_child_ids = [] - # Case A: Inline item layout loop - if "item" in children_meta and isinstance( - children_meta["item"], list - ): - item_comps = children_meta["item"] - as_var = children_meta.get("as") - for idx, item in enumerate(array_data): - sub_instance_id = f"{comp_copy['id']}_item_{idx}" - unrolled_child_ids.append(sub_instance_id) - - item_dict = ( - dict(item) - if isinstance(item, dict) - else {"value": item} - ) - sub_params = {**params, **item_dict} - if as_var: - sub_params[as_var] = item_dict - - inline_template = StaticTemplate( - template_id=f"{template_id}_inline_{idx}", - parameters={}, - components=item_comps, - ) - sub_expanded = self._expand_static_layout( - sub_instance_id, - inline_template, - sub_params, - _depth=_depth + 1, - _call_stack=_call_stack, - ) - expanded_components.extend(sub_expanded) - - # Case B: Named template loop - elif "template" in children_meta: - item_template_id = children_meta["template"] - for idx, item in enumerate(array_data): - sub_instance_id = f"{comp_copy['id']}_item_{idx}" - unrolled_child_ids.append(sub_instance_id) - sub_params = ( - item if isinstance(item, dict) else {"value": item} - ) - sub_expanded = self.expand_template( - sub_instance_id, - item_template_id, - sub_params, - _depth=_depth + 1, - _call_stack=_call_stack, - ) - expanded_components.extend(sub_expanded) + # Case A: Inline item layout loop + if "item" in children_meta and isinstance( + children_meta["item"], list + ): + item_comps = children_meta["item"] + as_var = children_meta.get("as") + for idx, item in enumerate(array_data): + sub_instance_id = f"{comp_copy['id']}_item_{idx}" + unrolled_child_ids.append(sub_instance_id) + + item_dict = ( + dict(item) + if isinstance(item, dict) + else {"value": item} + ) + sub_params = {**params, **item_dict} + if as_var: + sub_params[as_var] = item_dict + + inline_template = StaticTemplate( + template_id=f"{template_id}_inline_{idx}", + parameters={}, + components=item_comps, + ) + sub_expanded = self._expand_static_layout( + sub_instance_id, + inline_template, + sub_params, + _depth=_depth + 1, + _call_stack=_call_stack, + ) + expanded_components.extend(sub_expanded) + + # Case B: Named template loop + elif "template" in children_meta: + item_template_id = children_meta["template"] + for idx, item in enumerate(array_data): + sub_instance_id = f"{comp_copy['id']}_item_{idx}" + unrolled_child_ids.append(sub_instance_id) + sub_params = ( + item if isinstance(item, dict) else {"value": item} + ) + sub_expanded = self.expand_template( + sub_instance_id, + item_template_id, + sub_params, + _depth=_depth + 1, + _call_stack=_call_stack, + ) + expanded_components.extend(sub_expanded) - comp_copy["children"] = unrolled_child_ids + comp_copy["children"] = unrolled_child_ids else: comp_copy["children"] = self._map_child_list( comp_copy["children"], instance_id, params @@ -792,8 +797,8 @@ def process_message( if "components" in payload: components = payload["components"] - # Normalize single top-level template instances to root ID - if len(components) == 1: + # Normalize single top-level template instances to root ID in createSurface + if envelope_key == "createSurface" and len(components) == 1: single_comp = components[0] if ( isinstance(single_comp, dict) diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/README.md b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/README.md new file mode 100644 index 0000000000..0e63eeaedb --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/README.md @@ -0,0 +1,63 @@ +# A2UI Template Conformance Test Suite + +This directory contains the language-agnostic, data-driven conformance test suite for A2UI Templates. + +While currently maintained within the Python Agent SDK (`agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/`), the suite is defined entirely in declarative YAML and JSON Schema to enable seamless graduation to the root `conformance/templates/` directory as cross-platform SDKs (Dart, TypeScript, Kotlin) add template engines. + +--- + +## Directory Structure + +``` +conformance/ +├── schema/ +│ └── template_conformance_schema.json <-- JSON Schema validating all YAML test suites +├── suites/ +│ ├── 01_substitution.yaml <-- Exact match, interpolation, AST expressions, token escaping +│ ├── 02_static_expansion.yaml <-- Single/multi-child expansion, slots, data bindings +│ ├── 03_loop_unrolling.yaml <-- Inline loops, named loops, empty arrays, 2D unrolling +│ ├── 04_parameter_validation.yaml <-- Parameter type enforcement, enum validation, required fields +│ ├── 05_message_interception.yaml <-- createSurface, updateComponents, multi-envelope streams +│ └── 06_error_invariants.yaml <-- Unregistered templates, cycle guards, recursion limits +├── test_template_conformance.py <-- Pytest harness running the entire suite +└── README.md +``` + +--- + +## Action Runners + +Each test case declares an `action` attribute: + +1. **`substitute_params`**: + - Tests `_substitute_params(val, params)` directly. + - Asserts exact value and type preservation (integers, booleans, objects, arrays), string interpolation, AST expressions (`format`, `concat`), and literal escaping (`\${token}`). + +2. **`expand_template`**: + - Registers defined template(s) with `TemplateProcessor`. + - Executes `processor.expand_template(instance_id, template_id, args)`. + - Asserts exact component list structure and synthetic ID generation. + +3. **`process_message`**: + - Intercepts A2UI messages (`createSurface`, `updateComponents`). + - Asserts unrolling of template instances into standard primitive components. + +4. **`validate_template`**: + - Tests template ingestion and version validation (`version: "0.1"`). + - Asserts validation errors on invalid or missing version fields. + +--- + +## Graduation to Cross-Platform + +When A2UI Templates graduate from experimental to standard protocol: + +1. Move the entire `conformance/` directory to root: + ```bash + mv agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/ conformance/templates/ + ``` +2. The `suites/*.yaml` test definitions require **zero modifications**. +3. Implement standard harnesses in other target languages: + - **TypeScript / React / Lit**: Vitest harness reading `suites/*.yaml`. + - **Dart / Flutter**: `package:test` harness reading `suites/*.yaml`. + - **Kotlin**: JUnit5 harness reading `suites/*.yaml`. diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json new file mode 100644 index 0000000000..be01fc3da9 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/conformance/template_conformance_schema.json", + "title": "A2UI Template Conformance Test Suite Schema", + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "action" + ], + "properties": { + "name": { + "type": "string", + "description": "Unique name of the test case." + }, + "description": { + "type": "string", + "description": "Human-readable description of what this test case asserts." + }, + "action": { + "type": "string", + "enum": [ + "substitute_params", + "expand_template", + "process_message", + "validate_template", + "generate_catalog" + ], + "description": "The template engine action being tested." + }, + "templates": { + "type": "array", + "description": "List of template definitions registered with the processor." + }, + "template": { + "type": "object", + "description": "Single template definition registered with the processor." + }, + "instance_id": { + "type": "string", + "description": "Target component instance ID for expansion." + }, + "template_id": { + "type": "string", + "description": "Identifier of the template being expanded." + }, + "base_catalog": { + "type": [ + "object", + "null" + ], + "description": "Optional base catalog schema provided to processor." + }, + "args": { + "description": "Arguments/parameters passed to the action." + }, + "message": { + "description": "Message payload passed to process_message." + }, + "expect": { + "description": "Expected result matching the action return value." + }, + "expect_error": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "description": "Expected error category and error substring." + } + } + } +} \ No newline at end of file diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml new file mode 100644 index 0000000000..b6c2990704 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml @@ -0,0 +1,151 @@ +- name: test_exact_integer_preservation + description: Verifies that exact token match preserves integer type without string + coercion. + action: substitute_params + args: + value: ${count} + params: + count: 42 + expect: 42 +- name: test_exact_boolean_preservation + description: Verifies that exact token match preserves boolean type. + action: substitute_params + args: + value: ${isEnabled} + params: + isEnabled: true + expect: true +- name: test_exact_array_preservation + description: Verifies that exact token match preserves array data structure. + action: substitute_params + args: + value: ${tags} + params: + tags: + - admin + - developer + - tester + expect: + - admin + - developer + - tester +- name: test_exact_object_preservation + description: Verifies that exact token match preserves nested dictionary structure. + action: substitute_params + args: + value: ${binding} + params: + binding: + path: /session/user/name + expect: + path: /session/user/name +- name: test_in_string_interpolation + description: Verifies string interpolation when token appears with surrounding text. + action: substitute_params + args: + value: Hello, ${name}! + params: + name: Alice + expect: Hello, Alice! +- name: test_multiple_in_string_interpolations + description: Verifies multiple tokens interpolated in a single string. + action: substitute_params + args: + value: ${dept} - Grade ${grade} (${active}) + params: + dept: Engineering + grade: 7 + active: true + expect: Engineering - Grade 7 (True) +- name: test_token_escaping_exact + description: Verifies that leading backslash prevents substitution of exact token. + action: substitute_params + args: + value: \${keep_literal} + params: + keep_literal: not_used + expect: ${keep_literal} +- name: test_token_escaping_in_string + description: Verifies escaping in mixed string preserves literal token while substituting + unescaped token. + action: substitute_params + args: + value: Literal \${keep} and dynamic ${replace} + params: + keep: ignored + replace: substituted + expect: Literal ${keep} and dynamic substituted +- name: test_deep_dot_notation_nested_dict + description: Verifies resolving multi-level dot-separated path in nested dictionary. + action: substitute_params + args: + value: ${user.profile.details.tier} + params: + user: + profile: + details: + tier: Enterprise + expect: Enterprise +- name: test_deep_dot_notation_array_index + description: Verifies resolving dot-separated path with numeric array indices. + action: substitute_params + args: + value: ${teams.0.members.1.name} + params: + teams: + - members: + - name: First + - name: Second + expect: Second +- name: test_format_expression_ast + description: Verifies evaluating format expression dictionary with arguments. + action: substitute_params + args: + value: + format: 'Progress: {current} of {total} ({pct}%)' + args: + current: ${metrics.completed} + total: ${metrics.total} + pct: 75 + params: + metrics: + completed: 15 + total: 20 + expect: 'Progress: 15 of 20 (75%)' +- name: test_concat_expression_ast + description: Verifies evaluating concat expression combining string literals and + param references. + action: substitute_params + args: + value: + concat: + - 'Status: ' + - param: state + - ' (' + - param: code + - ) + params: + state: OK + code: 200 + expect: 'Status: OK (200)' +- name: test_concat_with_none_omits_part + description: Verifies that None or unresolved parts in concat do not insert 'None' + strings. + action: substitute_params + args: + value: + concat: + - Prefix- + - param: missing + - -Suffix + params: {} + expect: Prefix--Suffix +- name: test_param_ref_with_default + description: Verifies that direct param ref uses explicit default when param missing. + action: substitute_params + args: + value: + param: role + default: Contributor + params: {} + expect: Contributor diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml new file mode 100644 index 0000000000..65304f4bf0 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml @@ -0,0 +1,192 @@ +- name: test_single_child_card_expansion + description: Verifies expanding card container with single child and deterministic + ID prefixing. + action: expand_template + template: + version: '0.1' + templateId: SimpleCard + parameters: + title: + type: string + layout: + component: Card + child: + component: Text + text: ${title} + instance_id: card_inst + template_id: SimpleCard + args: + title: Welcome + expect: + - id: card_inst + component: Card + child: card_inst_child_text + - id: card_inst_child_text + component: Text + text: Welcome +- name: test_default_param_values + description: Verifies default value is assigned when argument is omitted. + action: expand_template + template: + version: '0.1' + templateId: ProfileHeader + parameters: + name: + type: string + role: + type: string + default: Team Member + layout: + component: Column + children: + - component: Text + text: ${name} + - component: Text + text: ${role} + instance_id: prof_1 + template_id: ProfileHeader + args: + name: Bob + expect: + - id: prof_1 + component: Column + children: + - prof_1_child_0_text + - prof_1_child_1_text + - id: prof_1_child_0_text + component: Text + text: Bob + - id: prof_1_child_1_text + component: Text + text: Team Member +- name: test_higher_order_child_slot + description: Verifies passing a component ID into a child slot parameter. + action: expand_template + template: + version: '0.1' + templateId: ContainerSlot + parameters: + content: + type: child + layout: + component: Card + child: ${content} + instance_id: slot_inst + template_id: ContainerSlot + args: + content: custom_chart_comp_id + expect: + - id: slot_inst + component: Card + child: custom_chart_comp_id +- name: test_unfilled_optional_child_slot_omitted + description: Verifies that when an optional child slot parameter is omitted, the + child property is omitted from output. + action: expand_template + template: + version: '0.1' + templateId: OptionalChildCard + parameters: + optionalChild: + type: child + required: false + layout: + component: Card + child: ${optionalChild} + instance_id: opt_card + template_id: OptionalChildCard + args: {} + expect: + - id: opt_card + component: Card +- name: test_higher_order_action_slot + description: Verifies binding an Action object to a button action slot via parameter. + action: expand_template + template: + version: '0.1' + templateId: ActionButton + parameters: + label: + type: string + onPress: + type: action + layout: + component: Button + text: ${label} + action: ${onPress} + instance_id: btn_inst + template_id: ActionButton + args: + label: Deploy + onPress: + name: triggerDeployment + parameters: + env: production + expect: + - id: btn_inst + component: Button + text: Deploy + action: + name: triggerDeployment + parameters: + env: production +- name: test_data_binding_passthrough + description: Verifies passing a client DataBinding dictionary through a parameter + preserving dictionary structure. + action: expand_template + template: + version: '0.1' + templateId: BoundMetric + parameters: + label: + type: string + valueBinding: + type: object + layout: + component: Text + text: ${valueBinding} + instance_id: bound_txt + template_id: BoundMetric + args: + label: CPU + valueBinding: + path: /system/cpu/usage + expect: + - id: bound_txt + component: Text + text: + path: /system/cpu/usage +- name: test_nested_template_invocation + description: Verifies a parent template layout referencing a child template expanding + recursively. + action: expand_template + templates: + - version: '0.1' + templateId: Badge + parameters: + text: + type: string + layout: + component: Text + text: '[${text}]' + - version: '0.1' + templateId: BadgeCard + parameters: + tag: + type: string + layout: + component: Card + child: + component: Badge + text: ${tag} + instance_id: parent_inst + template_id: BadgeCard + args: + tag: PRO + expect: + - id: parent_inst + component: Card + child: parent_inst_child_badge + - id: parent_inst_child_badge + component: Text + text: '[PRO]' diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml new file mode 100644 index 0000000000..369c94b3a1 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml @@ -0,0 +1,176 @@ +- name: test_inline_item_loop_unrolling + description: Verifies inline component layout unrolling for each element of an array. + action: expand_template + template: + version: '0.1' + templateId: MemberList + parameters: + members: + type: array + layout: + component: Column + children: + loop: + param: members + as: m + item: + component: Row + children: + - component: Text + text: ${m.name} + instance_id: team_col + template_id: MemberList + args: + members: + - name: Alice + - name: Bob + expect: + - id: team_col + component: Column + children: + - team_col_item_0 + - team_col_item_1 + - id: team_col_item_0 + component: Row + children: + - team_col_item_0_child_0_text + - id: team_col_item_0_child_0_text + component: Text + text: Alice + - id: team_col_item_1 + component: Row + children: + - team_col_item_1_child_0_text + - id: team_col_item_1_child_0_text + component: Text + text: Bob +- name: test_named_template_loop_unrolling + description: Verifies unrolling an array by invoking another registered template + for each element. + action: expand_template + templates: + - version: '0.1' + templateId: TagBadge + parameters: + label: + type: string + layout: + component: Text + text: '#${label}' + - version: '0.1' + templateId: TagContainer + parameters: + tags: + type: array + layout: + component: Row + children: + loop: + param: tags + template: TagBadge + instance_id: row_tags + template_id: TagContainer + args: + tags: + - label: alpha + - label: beta + expect: + - id: row_tags + component: Row + children: + - row_tags_item_0 + - row_tags_item_1 + - id: row_tags_item_0 + component: Text + text: '#alpha' + - id: row_tags_item_1 + component: Text + text: '#beta' +- name: test_empty_loop_array_emits_empty_children + description: Verifies that an empty array parameter unrolls cleanly to empty children + list. + action: expand_template + template: + version: '0.1' + templateId: EmptyListChecker + parameters: + items: + type: array + layout: + component: Column + children: + loop: + param: items + item: + component: Text + text: Item + instance_id: empty_col + template_id: EmptyListChecker + args: + items: [] + expect: + - id: empty_col + component: Column + children: [] +- name: test_primitive_list_iteration + description: Verifies iterating over primitive strings in an array using item.value + fallback. + action: expand_template + template: + version: '0.1' + templateId: ColorChips + parameters: + colors: + type: array + layout: + component: Row + children: + loop: + param: colors + as: c + item: + component: Text + text: ${c.value} + instance_id: chip_row + template_id: ColorChips + args: + colors: + - Crimson + - Azure + expect: + - id: chip_row + component: Row + children: + - chip_row_item_0 + - chip_row_item_1 + - id: chip_row_item_0 + component: Text + text: Crimson + - id: chip_row_item_1 + component: Text + text: Azure +- name: test_non_array_loop_parameter_raises_error + description: Verifies that passing a non-array string value to a loop raises a clear + ValidationError. + action: expand_template + template: + version: '0.1' + templateId: FailingLoop + parameters: + items: + type: array + layout: + component: Column + children: + loop: + param: items + item: + component: Text + text: Fail + instance_id: fail_inst + template_id: FailingLoop + args: + items: not_an_array + expect_error: + category: ValidationError + message: failed validation diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml new file mode 100644 index 0000000000..86f57ff0ec --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml @@ -0,0 +1,89 @@ +- name: test_parameter_type_validation_string_to_integer_fails + description: Verifies that passing a string to an integer parameter raises a validation + error. + action: expand_template + template: + version: '0.1' + templateId: IntegerParamCheck + parameters: + count: + type: integer + layout: + component: Text + text: 'Count: ${count}' + instance_id: int_fail + template_id: IntegerParamCheck + args: + count: non_integer_string + expect_error: + category: ValidationError + message: failed validation +- name: test_parameter_enum_validation_invalid_value_fails + description: Verifies that passing an unexpected value to an enum parameter raises + validation error. + action: expand_template + template: + version: '0.1' + templateId: EnumParamCheck + parameters: + status: + type: enum + values: + - ACTIVE + - SUSPENDED + - PENDING + layout: + component: Text + text: 'Status: ${status}' + instance_id: enum_fail + template_id: EnumParamCheck + args: + status: DELETED + expect_error: + category: ValidationError + message: failed validation +- name: test_missing_required_parameter_raises_error + description: Verifies that omitting a required parameter with no default raises + an error. + action: expand_template + template: + version: '0.1' + templateId: RequiredParamCheck + parameters: + mandatory: + type: string + layout: + component: Text + text: ${mandatory} + instance_id: req_fail + template_id: RequiredParamCheck + args: {} + expect_error: + category: ValidationError + message: Missing required parameter 'mandatory' +- name: test_unsupported_template_version_raises_error + description: Verifies that loading a template with unsupported version format raises + error. + action: validate_template + template: + version: '0.2' + templateId: FutureVersionTemplate + parameters: {} + layout: + component: Text + text: Future + expect_error: + category: ValidationError + message: Unsupported template version '0.2' +- name: test_missing_template_version_raises_error + description: Verifies that omitting version attribute completely raises error. + action: validate_template + template: + templateId: MissingVersionTemplate + parameters: {} + layout: + component: Text + text: No Version + expect_error: + category: ValidationError + message: missing required 'version' attribute diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml new file mode 100644 index 0000000000..4a53961267 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml @@ -0,0 +1,144 @@ +- name: test_create_surface_envelope_unwrapping + description: Verifies that createSurface message wraps template instance and unwraps + into primitive components. + action: process_message + template: + version: '0.1' + templateId: SimpleCard + parameters: + title: + type: string + layout: + component: Card + child: + component: Text + text: ${title} + message: + createSurface: + surfaceId: main + catalogId: https://a2ui.org/custom.json + components: + - id: root + component: SimpleCard + title: Unwrapped + expect: + createSurface: + surfaceId: main + catalogId: https://a2ui.org/custom.json + components: + - id: root + component: Card + child: root_child_text + - id: root_child_text + component: Text + text: Unwrapped +- name: test_update_components_envelope_unwrapping + description: Verifies that updateComponents message with template instance expands + correctly. + action: process_message + template: + version: '0.1' + templateId: SimpleCard + parameters: + title: + type: string + layout: + component: Card + child: + component: Text + text: ${title} + message: + updateComponents: + surfaceId: main + components: + - id: card_2 + component: SimpleCard + title: Updated + expect: + updateComponents: + surfaceId: main + components: + - id: card_2 + component: Card + child: card_2_child_text + - id: card_2_child_text + component: Text + text: Updated +- name: test_mixed_primitives_and_templates_in_message + description: Verifies that non-template primitive components pass through untouched + alongside expanded templates. + action: process_message + template: + version: '0.1' + templateId: SimpleCard + parameters: + title: + type: string + layout: + component: Card + child: + component: Text + text: ${title} + message: + updateComponents: + surfaceId: main + components: + - id: div_1 + component: Divider + axis: horizontal + - id: card_mixed + component: SimpleCard + title: Mixed Content + expect: + updateComponents: + surfaceId: main + components: + - id: div_1 + component: Divider + axis: horizontal + - id: card_mixed + component: Card + child: card_mixed_child_text + - id: card_mixed_child_text + component: Text + text: Mixed Content +- name: test_batch_messages_stream_passthrough + description: Verifies processing a list of messages passing dataModel updates through + unaltered. + action: process_message + template: + version: '0.1' + templateId: SimpleCard + parameters: + title: + type: string + layout: + component: Card + child: + component: Text + text: ${title} + message: + - updateDataModel: + path: /session/user + value: + name: Alice + - updateComponents: + surfaceId: main + components: + - id: batch_c + component: SimpleCard + title: Batch + expect: + - updateDataModel: + path: /session/user + value: + name: Alice + - updateComponents: + surfaceId: main + components: + - id: batch_c + component: Card + child: batch_c_child_text + - id: batch_c_child_text + component: Text + text: Batch diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml new file mode 100644 index 0000000000..6fbd0b7b20 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml @@ -0,0 +1,47 @@ +- name: test_unregistered_template_raises_error + description: Verifies expanding an unregistered template ID raises a clear ValueError. + action: expand_template + templates: [] + instance_id: inst_1 + template_id: UnknownTemplate + args: {} + expect_error: + category: ValidationError + message: Template 'UnknownTemplate' is not registered +- name: test_circular_reference_a_b_a + description: Verifies immediate detection of circular template references A -> B + -> A. + action: expand_template + templates: + - version: '0.1' + templateId: NodeA + parameters: {} + layout: + component: NodeB + - version: '0.1' + templateId: NodeB + parameters: {} + layout: + component: NodeA + instance_id: root_circ + template_id: NodeA + args: {} + expect_error: + category: ValidationError + message: Circular template reference detected +- name: test_circular_reference_self + description: Verifies that a template referencing itself is immediately caught by + the cycle guard. + action: expand_template + templates: + - version: '0.1' + templateId: SelfLoop + parameters: {} + layout: + component: SelfLoop + instance_id: root_self + template_id: SelfLoop + args: {} + expect_error: + category: ValidationError + message: Circular template reference detected diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py new file mode 100644 index 0000000000..75f1a04781 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py @@ -0,0 +1,156 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Conformance test runner for A2UI Templates executing declarative YAML suites.""" + +import glob +import json +import os +from pathlib import Path +from typing import Any, Dict, List, Tuple +import jsonschema +import pytest +import yaml + +from a2ui.inference_formats.experimental.template import ( + StaticTemplate, + TemplateProcessor, +) +from a2ui.inference_formats.experimental.template.processor import ( + _substitute_params, +) + + +def get_conformance_dir() -> Path: + """Dynamically resolves the conformance directory regardless of nesting.""" + curr = Path(__file__).resolve().parent + return curr / "conformance" + + +def load_conformance_schema() -> Dict[str, Any]: + schema_path = get_conformance_dir() / "schema" / "template_conformance_schema.json" + with open(schema_path, "r", encoding="utf-8") as f: + return json.load(f) + + +def load_all_conformance_cases() -> List[Tuple[str, str, Dict[str, Any]]]: + """Loads and validates all test cases across conformance suite YAML files.""" + schema = load_conformance_schema() + cases: List[Tuple[str, str, Dict[str, Any]]] = [] + suites_dir = get_conformance_dir() / "suites" + + for yaml_path in sorted(suites_dir.glob("*.yaml")): + suite_name = yaml_path.name + with open(yaml_path, "r", encoding="utf-8") as f: + raw_cases = yaml.safe_load(f) or [] + + # Validate the suite file against conformance JSON Schema + jsonschema.validate(instance=raw_cases, schema=schema) + + for case in raw_cases: + cases.append((suite_name, case["name"], case)) + + return cases + + +CONFORMANCE_CASES = load_all_conformance_cases() + + +def _execute_case(case: Dict[str, Any]) -> Any: + action = case["action"] + + if action == "substitute_params": + val = case["args"]["value"] + params = case["args"]["params"] + return _substitute_params(val, params) + + elif action == "expand_template": + templates: List[StaticTemplate] = [] + if "template" in case: + templates.append(StaticTemplate.from_dict(case["template"])) + elif "templates" in case: + for t_dict in case["templates"]: + templates.append(StaticTemplate.from_dict(t_dict)) + + base_catalog = case.get("base_catalog", None) + processor = TemplateProcessor(templates=templates, base_catalog=base_catalog) + + template_id = case.get( + "template_id", + templates[0].template_id if templates else "Unknown", + ) + instance_id = case.get("instance_id", "root") + args = case.get("args", {}) + + return processor.expand_template( + instance_id=instance_id, + template_id=template_id, + passed_params=args, + ) + + elif action == "process_message": + templates = [] + if "template" in case: + templates.append(StaticTemplate.from_dict(case["template"])) + elif "templates" in case: + for t_dict in case["templates"]: + templates.append(StaticTemplate.from_dict(t_dict)) + + base_catalog = case.get("base_catalog", None) + processor = TemplateProcessor(templates=templates, base_catalog=base_catalog) + + return processor.process_message(case["message"]) + + elif action == "validate_template": + return StaticTemplate.from_dict(case["template"]) + + raise ValueError(f"Unknown conformance test action: '{action}'") + + +@pytest.mark.parametrize("suite_name,case_name,case", CONFORMANCE_CASES) +def test_template_conformance( + suite_name: str, case_name: str, case: Dict[str, Any] +) -> None: + """Executes a single declarative conformance test case.""" + if "expect_error" in case: + expected_msg = case["expect_error"].get("message", "") + with pytest.raises(Exception) as excinfo: + _execute_case(case) + if expected_msg: + assert expected_msg.lower() in str(excinfo.value).lower(), ( + f"[{suite_name}::{case_name}] Expected error containing" + f" '{expected_msg}', got: {str(excinfo.value)}" + ) + else: + actual = _execute_case(case) + expected = case["expect"] + if ( + isinstance(actual, list) + and isinstance(expected, list) + and all(isinstance(x, dict) and "id" in x for x in actual) + and all(isinstance(x, dict) and "id" in x for x in expected) + ): + actual_sorted = sorted(actual, key=lambda x: str(x["id"])) + expected_sorted = sorted(expected, key=lambda x: str(x["id"])) + assert actual_sorted == expected_sorted, ( + f"[{suite_name}::{case_name}] Result mismatch:\n" + f"Expected: {expected_sorted}\n" + f"Actual: {actual_sorted}" + ) + else: + assert actual == expected, ( + f"[{suite_name}::{case_name}] Result mismatch:\n" + f"Expected: {expected}\n" + f"Actual: {actual}" + ) diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py index 619dfc97f3..d3cf6247ac 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py @@ -305,6 +305,7 @@ def test_template_inference_format_end_to_end(): def test_multi_document_yaml_loading_and_forward_references(): yaml_suite = """ +version: "0.1" templateId: RosterView description: Top-level roster view. parameters: @@ -324,6 +325,7 @@ def test_multi_document_yaml_loading_and_forward_references(): --- +version: "0.1" templateId: MemberCard description: Member identity card. parameters: @@ -341,7 +343,6 @@ def test_multi_document_yaml_loading_and_forward_references(): text: ${title} variant: caption """ - templates = StaticTemplate.from_yaml_string(yaml_suite) assert len(templates) == 2 assert templates[0].template_id == "RosterView" @@ -368,6 +369,7 @@ def test_multi_document_yaml_loading_and_forward_references(): def test_inline_loop_expansion(): yaml_tmpl = """ +version: "0.1" templateId: InlineListCard description: Card with inline unrolled loop items. parameters: @@ -418,6 +420,7 @@ def test_inline_loop_expansion(): def test_missing_template_reference_raises_clear_error(): yaml_tmpl = """ +version: "0.1" templateId: BadReference parameters: items: {type: array} @@ -440,6 +443,7 @@ def test_missing_template_reference_raises_clear_error(): def test_recursion_cycle_guard(): # Template A references Template B, and Template B references Template A yaml_suite = """ +version: "0.1" templateId: NodeA parameters: dummy: {type: string, default: "x"} @@ -450,6 +454,7 @@ def test_recursion_cycle_guard(): --- +version: "0.1" templateId: NodeB parameters: dummy: {type: string, default: "y"} @@ -467,6 +472,7 @@ def test_recursion_cycle_guard(): def test_non_text_parameter_type_preservation(): yaml_tmpl = """ +version: "0.1" templateId: TypedControls description: Card showing numbers, booleans, and objects. parameters: From 615c4c07e0ec00b9db1151fe31b432a76cccac1b Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 14:40:52 +0930 Subject: [PATCH 08/26] refactor(templates): remove unused manager.py placeholder --- .../experimental/template/manager.py | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py deleted file mode 100644 index 3b34254b45..0000000000 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/manager.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Optional, Any, Union -from a2ui.inference_format import InferenceFormat -from a2ui.core.schema.client_capabilities import V09Capabilities - - -class A2uiTemplateManager(InferenceFormat): - """Manages prompt compilation and payload processing for template definitions.""" - - @property - def parser(self) -> Any: - """The parser instance associated with the template manager.""" - raise NotImplementedError("This method is not yet implemented.") - - @property - def prompt_generator(self) -> Any: - """The prompt generator instance associated with the template manager.""" - raise NotImplementedError("This method is not yet implemented.") - - def generate_system_prompt( - self, - role_description: str, - workflow_description: str = "", - ui_description: str = "", - client_ui_capabilities: Optional[Union[dict[str, Any], V09Capabilities]] = None, - allowed_components: Optional[list[str]] = None, - allowed_messages: Optional[list[str]] = None, - include_schema: bool = False, - include_examples: bool = False, - validate_examples: bool = False, - ) -> str: - """Generates a system prompt for requests (not yet implemented).""" - # TODO: Implementation logic for Template Manager - raise NotImplementedError("This method is not yet implemented.") From 58b2c5f36144f4bc1cba740ba621b1fefb0b765f Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 14:48:09 +0930 Subject: [PATCH 09/26] test(templates): modularize Python unit tests into test_models, test_processor, and test_format --- .../experimental/template/test_format.py | 62 ++ .../experimental/template/test_models.py | 73 ++ .../experimental/template/test_processor.py | 296 ++++++ .../template/test_template_format.py | 958 ------------------ 4 files changed, 431 insertions(+), 958 deletions(-) create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py create mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py delete mode 100644 agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py new file mode 100644 index 0000000000..3449ed9d5f --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py @@ -0,0 +1,62 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for A2UI Template Inference Format (format.py) focusing on ADK framework integration.""" + +from a2ui.inference_formats.experimental.template import ( + StaticTemplate, + TemplateInferenceFormat, +) + + +def test_template_inference_format_end_to_end(): + """Verifies that TemplateInferenceFormat wraps ExpressFormat, generates prompts, and unrolls streaming chunks.""" + yaml_tmpl = """ +version: "0.1" +templateId: UserProfile +parameters: + userId: {type: string} + userName: {type: string} + role: {type: string} +layout: + component: Card + child: + component: Text + text: ${userName} +""" + tmpl = StaticTemplate.from_yaml(yaml_tmpl) + manager = TemplateInferenceFormat( + templates=[tmpl], + surface_id="main_surface", + version="0.9.1", + ) + + # 1. Check prompt generation includes UserProfile signature when include_schema=True + prompt = manager.prompt_generator.generate( + role_description="Test Agent", include_schema=True + ) + assert "UserProfile" in prompt + + # 2. Check parsing and expansion of Express DSL response + llm_output = """ + Here is your profile card: + + root = UserProfile("usr_99", "Sarah Jenkins", "Lead Designer") + + """ + + parts = manager.parser.parse_response(llm_output) + assert len(parts) == 1 + assert "Here is your profile card:" in parts[0].text + assert parts[0].a2ui_json is not None diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py new file mode 100644 index 0000000000..ba6b425a57 --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py @@ -0,0 +1,73 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for A2UI Template Models (models.py) focusing on Python runtime integration.""" + +from dataclasses import dataclass + +from a2ui.inference_formats.experimental.template.models import ( + DynamicTemplate, + ParamType, + normalize_node, +) + + +def test_normalize_node_dataclass_support(): + """Verifies that Python dataclass instances are normalized to plain dictionaries.""" + + @dataclass + class TextComponent: + component: str + text: str + + node = TextComponent(component="Text", text="Hello Dataclass") + normalized = normalize_node(node) + assert isinstance(normalized, dict) + assert normalized == {"component": "Text", "text": "Hello Dataclass"} + + +def test_normalize_node_duck_typing(): + """Verifies that objects implementing to_dict() are properly normalized.""" + + class CustomNode: + + def to_dict(self): + return {"component": "CustomCard", "score": 99} + + node = CustomNode() + normalized = normalize_node(node) + assert isinstance(normalized, dict) + assert normalized == {"component": "CustomCard", "score": 99} + + +def test_dynamic_template_signature_inference(): + """Verifies that DynamicTemplate infers typed parameters and defaults from Python callable annotations.""" + + def my_resolver(userId: str, count: int = 5, active: bool = True) -> dict: + return {} + + tmpl = DynamicTemplate(template_id="InferredTmpl", resolver=my_resolver) + params = tmpl.parameters + + assert "userId" in params + assert params["userId"].type == ParamType.STRING + assert params["userId"].default is None + + assert "count" in params + assert params["count"].type == ParamType.INTEGER + assert params["count"].default == 5 + + assert "active" in params + assert params["active"].type == ParamType.BOOLEAN + assert params["active"].default is True diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py new file mode 100644 index 0000000000..1161c76aae --- /dev/null +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py @@ -0,0 +1,296 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for A2UI Template Processor (processor.py) focusing on Python runtime execution and typing.""" + +import asyncio +from typing import Any, Dict, List +import pytest + +from a2ui.inference_formats.experimental.template import ( + DynamicTemplate, + StaticTemplate, + TemplateProcessor, + TemplateId, + InstanceId, + ComponentId, + ParamPath, + TemplateParams, + TemplateNodeDict, + A2UIComponent, + A2UIComponentList, + A2UIMessage, + CatalogSchema, + JSONSchemaDict, +) +from a2ui.inference_formats.experimental.template.processor import ( + _resolve_param_path, + _substitute_params, +) + + +def test_dynamic_template_with_resolver(): + """Verifies Mode B dynamic template where a native Python resolver fetches data and binds it to a static layout.""" + layout_yaml = """ +version: "0.1" +templateId: LiveStockCard +parameters: + ticker: {type: string} + price: {type: number, required: false} + changePct: {type: string, required: false} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: "Stock: ${ticker}" + - component: Text + text: "Price: $${price}" + - component: Text + text: "Change: ${changePct}%" +""" + + mock_db = { + "GOOG": {"price": 175.50, "changePct": "+1.8"}, + "AAPL": {"price": 220.10, "changePct": "-0.4"}, + } + + async def fetch_stock_data(ticker: str) -> Dict[str, Any]: + await asyncio.sleep(0.01) + return mock_db.get(ticker, {"price": 0.0, "changePct": "0.0"}) + + tmpl = DynamicTemplate( + template_id="LiveStockCard", + layout=layout_yaml, + resolver=fetch_stock_data, + description="Displays live stock pricing.", + ) + + processor = TemplateProcessor(templates=[tmpl]) + expanded = processor.expand_template( + "stock_goog", "LiveStockCard", {"ticker": "GOOG"} + ) + + assert len(expanded) == 5 + price_comp = next((c for c in expanded if c.get("text") == "Price: $175.5"), None) + assert price_comp is not None + + +def test_programmatic_dynamic_template_render(): + """Verifies Mode A dynamic template where a native Python callable directly constructs the component tree.""" + + def render_payroll(department: str = "Engineering", includeBonus: bool = True): + employees = [ + {"name": "Alice", "salary": 150000, "bonus": 20000}, + {"name": "Bob", "salary": 140000, "bonus": 15000}, + ] + rows = [] + for emp in employees: + cols = [{"component": "Text", "text": emp["name"]}] + if includeBonus: + cols.append({"component": "Text", "text": f"Bonus: ${emp['bonus']}"}) + rows.append({"component": "Row", "children": cols}) + + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + {"component": "Text", "text": f"Payroll: {department}"}, + *rows, + ], + }, + } + + tmpl = DynamicTemplate( + template_id="PayrollSummary", + render=render_payroll, + description="Dynamic payroll summary calculation.", + ) + + processor = TemplateProcessor(templates=[tmpl]) + expanded = processor.expand_template( + "payroll_root", "PayrollSummary", {"department": "AI Research"} + ) + + assert any(c.get("text") == "Payroll: AI Research" for c in expanded) + assert any(c.get("text") == "Bonus: $20000" for c in expanded) + + +def test_programmatic_dynamic_template_extra_params_filtered(): + """Verifies that dynamic render functions with explicit signatures do not break when receiving unexpected kwargs.""" + + def render_card(title: str): + return {"component": "Text", "text": title} + + tmpl = DynamicTemplate(template_id="SimpleCard", render=render_card) + processor = TemplateProcessor(templates=[tmpl]) + + expanded = processor.expand_template( + "inst", + "SimpleCard", + {"title": "Filtered", "extra1": "drop_me", "extra2": 42}, + ) + assert len(expanded) == 1 + assert expanded[0]["text"] == "Filtered" + + +def test_dynamic_template_resolve_non_dict_fallback(): + """Verifies graceful fallback when a resolver returns a non-dict value.""" + layout_yaml = """ +version: "0.1" +templateId: NonDictLayout +parameters: + paramA: {type: string} +layout: + component: Text + text: ${paramA} +""" + + def bad_resolver(paramA: str): + return "not a dict" + + tmpl = DynamicTemplate( + template_id="NonDictLayout", + layout=layout_yaml, + resolver=bad_resolver, + ) + processor = TemplateProcessor(templates=[tmpl]) + expanded = processor.expand_template("root", "NonDictLayout", {"paramA": "safe"}) + assert len(expanded) == 1 + assert expanded[0]["text"] == "safe" + + +def test_dynamic_template_loop_custom_as_variable(): + """Verifies dynamic template expansion utilizing custom loop variable naming.""" + layout_yaml = """ +version: "0.1" +templateId: CustomAsList +parameters: + items: {type: array} +layout: + component: Column + children: + loop: + param: items + as: item_var + item: + component: Text + text: ${item_var.label} +""" + tmpl = StaticTemplate.from_yaml(layout_yaml) + processor = TemplateProcessor(templates=[tmpl]) + expanded = processor.expand_template( + "root", + "CustomAsList", + {"items": [{"label": "First"}, {"label": "Second"}]}, + ) + texts = [c["text"] for c in expanded if c.get("component") == "Text"] + assert "First" in texts + assert "Second" in texts + + +def test_template_processor_completely_optional_base_catalog(): + """Verifies that TemplateProcessor works with base_catalog=None.""" + layout_yaml = """ +version: "0.1" +templateId: StandaloneTemplate +parameters: + title: {type: string} +layout: + component: Text + text: ${title} +""" + tmpl = StaticTemplate.from_yaml(layout_yaml) + processor = TemplateProcessor(templates=[tmpl], base_catalog=None) + + assert processor.base_catalog is None + assert processor.base_catalog_id is None + + cat = processor.generate_inference_catalog() + assert "StandaloneTemplate" in cat["components"] + assert "Card" not in cat["components"] + assert "Text" not in cat["components"] + + expanded = processor.expand_template( + "inst_1", "StandaloneTemplate", {"title": "Autonomous"} + ) + assert len(expanded) == 1 + assert expanded[0]["text"] == "Autonomous" + + +def test_template_processor_with_custom_domain_catalog(): + """Verifies TemplateProcessor when initialized with a custom domain catalog.""" + custom_catalog = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://company.org/custom_medical_catalog.json", + "title": "Medical Component Catalog", + "components": { + "PatientChart": {"type": "object"}, + "VitalsGauge": {"type": "object"}, + }, + "functions": {}, + } + + layout_yaml = """ +version: "0.1" +templateId: MedicalReport +parameters: + patientName: {type: string} +layout: + component: PatientChart + properties: + name: ${patientName} +""" + tmpl = StaticTemplate.from_yaml(layout_yaml) + processor = TemplateProcessor(templates=[tmpl], base_catalog=custom_catalog) + + assert ( + processor.base_catalog_id == "https://company.org/custom_medical_catalog.json" + ) + + cat = processor.generate_inference_catalog() + assert "PatientChart" in cat["components"] + assert "VitalsGauge" in cat["components"] + assert "MedicalReport" in cat["components"] + assert "Card" not in cat["components"] + assert "Button" not in cat["components"] + + +def test_type_aliases_and_private_helpers(): + """Verifies that semantic type aliases and backwards-compatible private functions are available.""" + assert TemplateId is str + assert InstanceId is str + assert ComponentId is str + assert ParamPath is str + + # Test private helpers + params: TemplateParams = {"user": {"profile": {"tier": "Pro"}}} + found, val = _resolve_param_path("user.profile.tier", params) + assert found is True + assert val == "Pro" + + sub = _substitute_params("${user.profile.tier}", params) + assert sub == "Pro" + + t = StaticTemplate( + template_id="Dummy", + version="0.1", + parameters={}, + components=[], + ) + p = TemplateProcessor(templates=[t]) + p._validate_templates() + p.validate_templates() # test deprecated public alias diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py deleted file mode 100644 index d3cf6247ac..0000000000 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_format.py +++ /dev/null @@ -1,958 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from pathlib import Path -import pytest -from a2ui.inference_formats.experimental.template import ( - Template, - StaticTemplate, - DynamicTemplate, - Param, - ParamType, - ParamRef, - Concat, - TemplateComponent, - TemplateLoop, - TemplateProcessor, - TemplateInferenceFormat, - A2uiTemplateManager, - flatten_nested_layout, - normalize_node, - A2UIComponent, - A2UIComponentList, - TemplateParams, - TemplateId, - InstanceId, -) - - -def get_examples_dir() -> str: - curr = Path(__file__).resolve() - while curr != curr.parent: - if (curr / "specification").exists(): - return str(curr / "specification" / "proposals" / "templates" / "examples") - curr = curr.parent - raise RuntimeError("Could not locate repo root containing 'specification'") - - -def load_example(name: str) -> Template: - file_path = os.path.join(get_examples_dir(), f"{name}.yaml") - return Template.from_yaml_file(file_path)[0] - - -def test_user_profile_template_expansion(): - user_profile = load_example("user_profile") - processor = TemplateProcessor(templates=[user_profile]) - - expanded = processor.expand_template( - instance_id="alice_card", - template_id="UserProfile", - passed_params={ - "userId": "u123", - "userName": "Alice Smith", - "role": "Admin", - }, - ) - - assert len(expanded) == 5 - - card = next(c for c in expanded if c["id"] == "alice_card") - assert card["component"] == "Card" - - name_text = next( - c - for c in expanded - if "text" in c and c.get("component") == "Text" and c["text"] == "Alice Smith" - ) - assert name_text is not None - - role_text = next( - c - for c in expanded - if "text" in c and c.get("component") == "Text" and c["text"] == "Admin" - ) - assert role_text is not None - - -def test_user_profile_default_role(): - user_profile = load_example("user_profile") - processor = TemplateProcessor(templates=[user_profile]) - - expanded = processor.expand_template( - instance_id="bob_card", - template_id="UserProfile", - passed_params={ - "userId": "u456", - "userName": "Bob", - }, - ) - - role_text = next(c for c in expanded if "text" in c and c["text"] == "Member") - assert role_text is not None - - -def test_nested_team_card_loop_unrolling(): - user_profile = load_example("user_profile") - team_card = load_example("team_card") - processor = TemplateProcessor(templates=[user_profile, team_card]) - - data = { - "teamName": "Core Architecture", - "members": [ - {"userId": "u1", "userName": "Elena", "role": "Principal"}, - {"userId": "u2", "userName": "Marcus"}, - ], - } - - expanded = processor.expand_template( - instance_id="arch_team", - template_id="TeamCard", - passed_params=data, - ) - - root_card = next(c for c in expanded if c["id"] == "arch_team") - assert root_card["component"] == "Card" - - # Elena's name - elena_name = next(c for c in expanded if "text" in c and c["text"] == "Elena") - assert elena_name is not None - - # Marcus default role - marcus_role = next(c for c in expanded if "text" in c and c["text"] == "Member") - assert marcus_role is not None - - -def test_team_roster_template(): - user_profile = load_example("user_profile") - team_card = load_example("team_card") - team_roster = load_example("team_roster") - - processor = TemplateProcessor(templates=[user_profile, team_card, team_roster]) - - expanded = processor.expand_template( - instance_id="roster", - template_id="TeamRoster", - passed_params={ - "directoryTitle": "Global Directory", - "children": ["team_1", "team_2"], - }, - ) - - root = next(c for c in expanded if c["id"] == "roster") - assert root["component"] == "Column" - - -def test_goal_item_and_team_goal_list(): - goal_item = load_example("goal_item") - team_goal_list = load_example("team_goal_list") - - processor = TemplateProcessor(templates=[goal_item, team_goal_list]) - - expanded = processor.expand_template( - instance_id="team_goals", - template_id="TeamGoalList", - passed_params={ - "teamName": "Protocol Team", - "goals": [ - { - "title": "Upgrade Agent SDK", - "priority": "High", - "targetDate": "2026-08-30", - }, - {"title": "Clean Demos", "targetDate": "2026-09-15"}, - ], - }, - ) - - assert any(c["id"] == "team_goals" for c in expanded) - assert any("Upgrade Agent SDK" in str(c.get("text")) for c in expanded) - - -def test_feedback_item_and_board(): - feedback_item = load_example("feedback_item") - feedback_board = load_example("team_feedback_board") - - processor = TemplateProcessor(templates=[feedback_item, feedback_board]) - - expanded = processor.expand_template( - instance_id="board", - template_id="TeamFeedbackBoard", - passed_params={ - "teamName": "Core Engineers", - "feedbacks": [ - {"author": "Alice", "note": "Great synchronous speed!", "rating": 5}, - ], - }, - ) - - assert any(c["id"] == "board" for c in expanded) - assert any("Great synchronous speed!" in str(c.get("text")) for c in expanded) - - -def test_section_card_with_slots(): - section_card = load_example("section_card") - processor = TemplateProcessor(templates=[section_card]) - - expanded = processor.expand_template( - instance_id="sec", - template_id="SectionCard", - passed_params={ - "title": "Settings", - "description": "Configure your options", - "headerAction": "settings_btn", - "children": ["opt_1", "opt_2"], - }, - ) - - action_slot = next(c for c in expanded if c["id"] == "sec_action_slot") - assert action_slot["children"] == ["settings_btn"] - - body_container = next(c for c in expanded if c["id"] == "sec_body_container") - assert body_container["children"] == ["opt_1", "opt_2"] - - -def test_process_message_create_surface(): - user_profile = load_example("user_profile") - processor = TemplateProcessor(templates=[user_profile]) - - msg = { - "version": "v0.9", - "createSurface": { - "surfaceId": "user_view", - "components": [{ - "id": "my_user", - "component": "UserProfile", - "userId": "usr_999", - "userName": "Sarah Connor", - "role": "Defender", - }], - }, - } - - processed = processor.process_message(msg) - components = processed["createSurface"]["components"] - assert len(components) == 5 - assert components[0]["id"] == "root" - assert components[0]["component"] == "Card" - - -def test_template_inference_format_end_to_end(): - user_profile = load_example("user_profile") - manager = TemplateInferenceFormat( - templates=[user_profile], - surface_id="main_surface", - version="0.9.1", - ) - - # 1. Check prompt generation includes UserProfile signature when include_schema=True - prompt = manager.prompt_generator.generate( - role_description="Test Agent", include_schema=True - ) - assert "UserProfile" in prompt - - # 2. Check parsing and expansion of Express DSL response - llm_output = """ - Here is your profile card: - - root = UserProfile("usr_99", "Sarah Jenkins", "Lead Designer") - - """ - - parts = manager.parser.parse_response(llm_output) - assert len(parts) == 1 - assert "Here is your profile card:" in parts[0].text - assert parts[0].a2ui_json is not None - - messages = parts[0].a2ui_json - assert len(messages) == 2 - - # In v0.9.1, createSurface and updateComponents are separate - create_surface = messages[0].get("createSurface") - assert create_surface is not None - assert create_surface["surfaceId"] == "main_surface" - - update_components = messages[1].get("updateComponents") - assert update_components is not None - assert update_components["surfaceId"] == "main_surface" - - components = update_components["components"] - assert len(components) == 5 - name_text = next( - c - for c in components - if c.get("component") == "Text" and c.get("text") == "Sarah Jenkins" - ) - assert name_text is not None - role_text = next( - c - for c in components - if c.get("component") == "Text" and c.get("text") == "Lead Designer" - ) - assert role_text is not None - - -def test_multi_document_yaml_loading_and_forward_references(): - yaml_suite = """ -version: "0.1" -templateId: RosterView -description: Top-level roster view. -parameters: - title: {type: string} - members: {type: array} -layout: - component: Column - children: - - component: Text - text: ${title} - variant: h1 - - component: Column - children: - loop: - param: members - template: MemberCard # Forward reference to MemberCard below! - ---- - -version: "0.1" -templateId: MemberCard -description: Member identity card. -parameters: - name: {type: string} - title: {type: string} -layout: - component: Card - child: - component: Column - children: - - component: Text - text: ${name} - variant: h3 - - component: Text - text: ${title} - variant: caption -""" - templates = StaticTemplate.from_yaml_string(yaml_suite) - assert len(templates) == 2 - assert templates[0].template_id == "RosterView" - assert templates[1].template_id == "MemberCard" - - # Expand RosterView (which forward-references MemberCard) - processor = TemplateProcessor(templates=templates) - expanded = processor.expand_template( - "inst_1", - "RosterView", - { - "title": "Engineering Team", - "members": [ - {"name": "Alice", "title": "Staff Engineer"}, - {"name": "Bob", "title": "Tech Lead"}, - ], - }, - ) - - assert any(c.get("text") == "Engineering Team" for c in expanded) - assert any(c.get("text") == "Alice" for c in expanded) - assert any(c.get("text") == "Bob" for c in expanded) - - -def test_inline_loop_expansion(): - yaml_tmpl = """ -version: "0.1" -templateId: InlineListCard -description: Card with inline unrolled loop items. -parameters: - teamName: {type: string} - skills: {type: array} -layout: - component: Card - child: - component: Column - children: - - component: Text - text: ${teamName} - variant: h2 - - component: Column - children: - loop: - param: skills - as: skill - item: - component: Row - justify: spaceBetween - children: - - component: Text - text: ${skill.name} - - component: Text - text: "Lvl ${skill.level}" -""" - - template = StaticTemplate.from_yaml(yaml_tmpl) - processor = TemplateProcessor(templates=[template]) - - expanded = processor.expand_template( - "inst_inline", - "InlineListCard", - { - "teamName": "Frontend Guild", - "skills": [ - {"name": "TypeScript", "level": "Expert"}, - {"name": "A2UI Protocols", "level": "Master"}, - ], - }, - ) - - assert any(c.get("text") == "Frontend Guild" for c in expanded) - assert any(c.get("text") == "TypeScript" for c in expanded) - assert any(c.get("text") == "Lvl Master" for c in expanded) - - -def test_missing_template_reference_raises_clear_error(): - yaml_tmpl = """ -version: "0.1" -templateId: BadReference -parameters: - items: {type: array} -layout: - component: Column - children: - loop: - param: items - template: NonExistentTemplate -""" - tmpl = StaticTemplate.from_yaml(yaml_tmpl) - processor = TemplateProcessor(templates=[tmpl]) - - with pytest.raises( - ValueError, match="Template 'NonExistentTemplate' is not registered" - ): - processor.expand_template("inst_bad", "BadReference", {"items": [{"id": 1}]}) - - -def test_recursion_cycle_guard(): - # Template A references Template B, and Template B references Template A - yaml_suite = """ -version: "0.1" -templateId: NodeA -parameters: - dummy: {type: string, default: "x"} -layout: - component: Column - children: - - component: NodeB - ---- - -version: "0.1" -templateId: NodeB -parameters: - dummy: {type: string, default: "y"} -layout: - component: Column - children: - - component: NodeA -""" - templates = StaticTemplate.from_yaml_string(yaml_suite) - processor = TemplateProcessor(templates=templates) - - with pytest.raises(ValueError, match="Circular template reference detected"): - processor.expand_template("cycle_root", "NodeA", {}) - - -def test_non_text_parameter_type_preservation(): - yaml_tmpl = """ -version: "0.1" -templateId: TypedControls -description: Card showing numbers, booleans, and objects. -parameters: - count: {type: integer} - isActive: {type: boolean} - actionObj: {type: action} -layout: - component: Card - child: - component: Column - children: - - component: Slider - value: ${count} - disabled: ${isActive} - - component: Button - onClick: ${actionObj} -""" - - tmpl = StaticTemplate.from_yaml(yaml_tmpl) - processor = TemplateProcessor(templates=[tmpl]) - - expanded = processor.expand_template( - "ctrl_1", - "TypedControls", - { - "count": 42, - "isActive": False, - "actionObj": {"actionId": "submit_form", "context": {"user": "alice"}}, - }, - ) - - slider = next(c for c in expanded if c.get("component") == "Slider") - assert slider["value"] == 42 - assert type(slider["value"]) is int - assert slider["disabled"] is False - assert type(slider["disabled"]) is bool - - btn = next(c for c in expanded if c.get("component") == "Button") - assert btn["onClick"] == {"actionId": "submit_form", "context": {"user": "alice"}} - assert type(btn["onClick"]) is dict - - -def test_dynamic_template_with_resolver(): - salary_layout = load_example("salary_card") - - # Mock database lookup resolver - def resolve_salary(employeeId: str): - db = { - "emp_101": { - "employeeName": "Dr. Elena Vance", - "role": "Principal Systems Architect", - "baseSalary": "$215,000", - "annualBonus": "$45,000", - "equity": "3,500 RSUs", - "clearanceLevel": "Level 5 - Confidential", - "verifiedAt": "2026-08-13", - } - } - if employeeId not in db: - raise ValueError(f"Unknown employee {employeeId}") - return db[employeeId] - - dynamic_tmpl = DynamicTemplate( - template_id="EmployeeSalaryCard", - resolver=resolve_salary, - layout=salary_layout, - description="Secure employee salary card resolved server-side.", - ) - - assert "employeeId" in dynamic_tmpl.parameters - assert dynamic_tmpl.is_dynamic is True - - processor = TemplateProcessor(templates=[dynamic_tmpl]) - synthetic_cat = processor.generate_inference_catalog() - assert "EmployeeSalaryCard" in synthetic_cat["components"] - - expanded = processor.expand_template( - "salary_inst", "EmployeeSalaryCard", {"employeeId": "emp_101"} - ) - assert len(expanded) > 5 - - name_txt = next(c for c in expanded if c.get("text") == "Dr. Elena Vance") - assert name_txt is not None - - sal_val = next(c for c in expanded if c.get("text") == "$215,000") - assert sal_val is not None - - -def test_programmatic_dynamic_template_render(): - def render_server_health(serverId: str, includeDisks: bool = True): - status_icon = "check_circle" if serverId == "srv_01" else "warning" - - disk_items = [] - if includeDisks: - mounts = [("/", 45), ("/data", 82), ("/var/log", 12)] - for mount, usage in mounts: - disk_items.append({ - "component": "Row", - "justify": "spaceBetween", - "children": [ - {"component": "Text", "text": mount, "variant": "caption"}, - { - "component": "Text", - "text": f"{usage}% used", - "variant": "caption", - }, - ], - }) - - return { - "component": "Card", - "child": { - "component": "Column", - "children": [ - { - "component": "Row", - "justify": "spaceBetween", - "align": "center", - "children": [ - { - "component": "Text", - "text": f"Server {serverId}", - "variant": "h3", - }, - {"component": "Icon", "name": status_icon}, - ], - }, - {"component": "Divider", "axis": "horizontal"}, - {"component": "Text", "text": "CPU Load: 24.5%", "variant": "body"}, - {"component": "Column", "children": disk_items}, - ], - }, - } - - dyn_tmpl = DynamicTemplate( - template_id="LiveServerHealth", - render=render_server_health, - description="Live server diagnostics.", - ) - - assert "serverId" in dyn_tmpl.parameters - assert "includeDisks" in dyn_tmpl.parameters - - processor = TemplateProcessor(templates=[dyn_tmpl]) - expanded = processor.expand_template( - "srv_dash", - "LiveServerHealth", - {"serverId": "srv_01", "includeDisks": True}, - ) - - assert len(expanded) > 6 - root_card = next(c for c in expanded if c["id"] == "srv_dash") - assert root_card["component"] == "Card" - - server_title = next(c for c in expanded if c.get("text") == "Server srv_01") - assert server_title is not None - - disk_entry = next(c for c in expanded if c.get("text") == "82% used") - assert disk_entry is not None - - -def test_programmatic_dynamic_template_duck_typing(): - # Mock class that implements .to_dict() protocol - class MockCustomBadge: - - def __init__(self, label: str): - self.label = label - - def to_dict(self): - return { - "component": "Card", - "child": { - "component": "Text", - "text": f"[ {self.label} ]", - "variant": "caption", - }, - } - - def render_badge(tag: str): - return MockCustomBadge(tag) - - dyn_tmpl = DynamicTemplate( - template_id="CustomBadgeView", - render=render_badge, - ) - - processor = TemplateProcessor(templates=[dyn_tmpl]) - expanded = processor.expand_template( - "badge_inst", "CustomBadgeView", {"tag": "ACTIVE"} - ) - - root = next(c for c in expanded if c["id"] == "badge_inst") - assert root["component"] == "Card" - text_node = next(c for c in expanded if c.get("text") == "[ ACTIVE ]") - assert text_node is not None - - -def test_template_versioning_valid(): - yaml_content = """ -version: "0.1" -templateId: SimpleVersionedTest -parameters: - title: - type: string -layout: - component: Text - text: ${title} -""" - tmpl = StaticTemplate.from_yaml(yaml_content) - assert tmpl.version == "0.1" - d = tmpl.to_dict() - assert d["version"] == "0.1" - assert "version: '0.1'" in tmpl.to_yaml() or 'version: "0.1"' in tmpl.to_yaml() - - -def test_template_versioning_invalid(): - yaml_content = """ -version: "0.2" -templateId: InvalidVersionTest -parameters: - title: - type: string -layout: - component: Text - text: ${title} -""" - with pytest.raises(ValueError, match="Unsupported template version"): - StaticTemplate.from_yaml(yaml_content) - - -def test_dynamic_template_resolve_non_dict_fallback(): - # Resolver returns None instead of a dict - def bad_resolver(): - return None - - layout = StaticTemplate.from_yaml(""" -version: "0.1" -templateId: LayoutForBadResolver -parameters: - name: - type: string - default: FallbackName -layout: - component: Text - text: ${name} -""") - - dyn_tmpl = DynamicTemplate( - template_id="BadResolverTemplate", - resolver=bad_resolver, - layout=layout, - ) - processor = TemplateProcessor(templates=[dyn_tmpl]) - # Should not raise TypeError: unsupported operand type(s) for **: 'NoneType' - expanded = processor.expand_template("bad_inst", "BadResolverTemplate", {}) - assert any(c.get("text") == "FallbackName" for c in expanded) - - -def test_template_data_binding_expression_on_primitive_param(): - yaml_content = """ -version: "0.1" -templateId: PrimitiveWithBinding -parameters: - label: - type: string -layout: - component: Text - text: ${label} -""" - tmpl = StaticTemplate.from_yaml(yaml_content) - processor = TemplateProcessor(templates=[tmpl]) - # Pass an A2UI DataBinding dict to a primitive string param - expanded = processor.expand_template( - "inst_1", "PrimitiveWithBinding", {"label": {"path": "/session/userName"}} - ) - text_node = next(c for c in expanded if c["id"] == "inst_1") - assert text_node["text"] == {"path": "/session/userName"} - - -def test_programmatic_dynamic_template_extra_params_filtered(): - # Function that takes only 'title' without **kwargs - def strict_render(title: str): - return { - "component": "Text", - "text": title, - } - - dyn_tmpl = DynamicTemplate( - template_id="StrictTitleView", - render=strict_render, - ) - processor = TemplateProcessor(templates=[dyn_tmpl]) - # Pass extra parameter 'extra_field' that strict_render doesn't accept - expanded = processor.expand_template( - "strict_inst", - "StrictTitleView", - {"title": "Hello World", "extra_field": 12345}, - ) - text_node = next(c for c in expanded if c["id"] == "strict_inst") - assert text_node["text"] == "Hello World" - - -def test_loop_definition_anyof_validation(): - # Loop missing both 'template' and 'item' - invalid_yaml = """ -version: "0.1" -templateId: InvalidLoopTemplate -parameters: - items: - type: array -layout: - component: Column - children: - loop: - param: items -""" - with pytest.raises(ValueError, match="fails template_definition.json schema"): - StaticTemplate.from_yaml(invalid_yaml) - - -def test_circular_template_reference_detection(): - # Template A references Template B which references Template A - tmpl_a = StaticTemplate.from_yaml(""" -version: "0.1" -templateId: CircularA -parameters: {} -layout: - component: CircularB -""") - - tmpl_b = StaticTemplate.from_yaml(""" -version: "0.1" -templateId: CircularB -parameters: {} -layout: - component: CircularA -""") - - processor = TemplateProcessor(templates=[tmpl_a, tmpl_b]) - with pytest.raises(ValueError, match="Circular template reference detected"): - processor.expand_template("inst_circ", "CircularA", {}) - - -def test_normalize_node_dataclass_support(): - from dataclasses import dataclass - - @dataclass - class CustomComponent: - component: str - text: str - - node = CustomComponent(component="Text", text="Hello Dataclass") - d = normalize_node(node) - assert isinstance(d, dict) - assert d["component"] == "Text" - assert d["text"] == "Hello Dataclass" - - -def test_dynamic_template_loop_custom_as_variable(): - # Verify that custom 'as: member' works without failing validation - custom_loop_yaml = """ -version: "0.1" -templateId: CustomLoopVarTemplate -parameters: - members: - type: array -layout: - component: Column - children: - loop: - param: members - as: member - item: - component: Text - text: ${member.name} -""" - tmpl = StaticTemplate.from_yaml(custom_loop_yaml) - assert tmpl.template_id == "CustomLoopVarTemplate" - - -def test_template_processor_completely_optional_base_catalog(): - tmpl = StaticTemplate.from_yaml(""" -version: "0.1" -templateId: PureTemplate -parameters: - title: {type: string} -layout: - component: Card - child: - component: Text - text: ${title} -""") - # Initialize processor with NO base catalog whatsoever - processor = TemplateProcessor(templates=[tmpl]) - assert processor.base_catalog is None - assert processor.base_catalog_id is None - - # Synthetic catalog has no hardcoded primitives, only registered template - cat = processor.generate_inference_catalog() - assert "PureTemplate" in cat["components"] - assert "Card" not in cat["components"] - assert "Text" not in cat["components"] - - # Expansion works flawlessly - expanded = processor.expand_template( - "pure_inst", "PureTemplate", {"title": "Autonomous"} - ) - assert len(expanded) == 2 - assert any(c.get("text") == "Autonomous" for c in expanded) - - # Process message does not inject any catalogId if base_catalog_id is None - msg = { - "createSurface": { - "surfaceId": "s1", - "catalogId": "client_custom_catalog", - "components": [], - } - } - out_msg = processor.process_message(msg) - assert out_msg["createSurface"]["catalogId"] == "client_custom_catalog" - - -def test_template_processor_with_custom_domain_catalog(): - # Catalog from custom domain with zero basic catalog references - custom_catalog = { - "$id": "https://company.org/custom_medical_catalog.json", - "components": { - "PatientChart": {"type": "object"}, - "VitalsGauge": {"type": "object"}, - }, - "functions": {}, - } - - tmpl = StaticTemplate.from_yaml(""" -version: "0.1" -templateId: MedicalReport -parameters: - patientName: {type: string} -layout: - component: PatientChart - properties: - name: ${patientName} -""") - - processor = TemplateProcessor(templates=[tmpl], base_catalog=custom_catalog) - assert ( - processor.base_catalog_id == "https://company.org/custom_medical_catalog.json" - ) - - # Inference catalog dynamically discovers custom catalog components - cat = processor.generate_inference_catalog() - assert "PatientChart" in cat["components"] - assert "VitalsGauge" in cat["components"] - assert "MedicalReport" in cat["components"] - # No standard Basic Catalog primitives injected - assert "Card" not in cat["components"] - assert "Button" not in cat["components"] - - # Process message properly rewrites surface catalogId to custom catalog ID - msg = { - "createSurface": { - "surfaceId": "med_surf", - "catalogId": "placeholder", - "components": [], - } - } - out_msg = processor.process_message(msg) - assert ( - out_msg["createSurface"]["catalogId"] - == "https://company.org/custom_medical_catalog.json" - ) - - -def test_type_aliases_and_private_helpers(): - params: TemplateParams = {"user": {"profile": {"tier": "Pro"}}} - from a2ui.inference_formats.experimental.template.processor import _resolve_param_path, _substitute_params - - found, val = _resolve_param_path("user.profile.tier", params) - assert found is True - assert val == "Pro" - - sub = _substitute_params("${user.profile.tier}", params) - assert sub == "Pro" From 78f89e620e256de1ea1081287c8d58f8b67b1b05 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 15:17:31 +0930 Subject: [PATCH 10/26] refactor(templates): refer to template_definition.json from specification/proposals/templates/schema --- agent_sdks/python/a2ui_agent/README.md | 2 +- .../experimental/template/models.py | 45 ++- .../template/schema/template_definition.json | 273 ------------------ 3 files changed, 35 insertions(+), 285 deletions(-) delete mode 100644 agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json diff --git a/agent_sdks/python/a2ui_agent/README.md b/agent_sdks/python/a2ui_agent/README.md index 67dd34caaf..7f26d935db 100644 --- a/agent_sdks/python/a2ui_agent/README.md +++ b/agent_sdks/python/a2ui_agent/README.md @@ -33,7 +33,7 @@ The following directories contain the base protocol logic, parsing, and schema o - **`models.py`**: Typed models (`StaticTemplate`, `DynamicTemplate`, `Param`, `ParamType`, AST expressions) and signature-based parameter inference. - **`processor.py`**: `TemplateProcessor` for generating synthetic inference catalogs and expanding template invocations into A2UI primitives. - **`format.py`**: `TemplateInferenceFormat` integrating template-aware prompt generation and synchronous parsing. -- **`schema/template_definition.json`**: Formal JSON Schema for template structure and parameter types. +- Reference schema: Located at `specification/proposals/templates/schema/template_definition.json`. ## A2A (`src/a2ui/a2a`) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py index 70c532ffc5..23841222c3 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -28,9 +28,29 @@ import jsonschema import yaml -SCHEMA_PATH = Path(__file__).parent / "schema" / "template_definition.json" -with open(SCHEMA_PATH, "r", encoding="utf-8") as _f: - TEMPLATE_DEFINITION_SCHEMA = json.load(_f) + +def _find_template_schema_path() -> Optional[Path]: + """Finds the template definition JSON schema in specification/proposals/templates/schema/.""" + current = Path(__file__).resolve() + for parent in current.parents: + candidate = ( + parent + / "specification" + / "proposals" + / "templates" + / "schema" + / "template_definition.json" + ) + if candidate.is_file(): + return candidate + return None + + +SCHEMA_PATH = _find_template_schema_path() +TEMPLATE_DEFINITION_SCHEMA: Optional[Dict[str, Any]] = None +if SCHEMA_PATH and SCHEMA_PATH.is_file(): + with open(SCHEMA_PATH, "r", encoding="utf-8") as _f: + TEMPLATE_DEFINITION_SCHEMA = json.load(_f) class ParamType(str, Enum): @@ -669,14 +689,17 @@ def _check_path(self, path: str, loop_vars: set) -> None: def validate_definition(self) -> None: """Statically validates the template against the JSON Schema, verifies parameter references, and validates sampleData.""" - raw_dict = self.to_dict() - try: - jsonschema.validate(instance=raw_dict, schema=TEMPLATE_DEFINITION_SCHEMA) - except jsonschema.ValidationError as err: - raise ValueError( - f"Template '{self.template_id}' fails template_definition.json schema:" - f" {err.message}" - ) from err + if TEMPLATE_DEFINITION_SCHEMA is not None: + raw_dict = self.to_dict() + try: + jsonschema.validate( + instance=raw_dict, schema=TEMPLATE_DEFINITION_SCHEMA + ) + except jsonschema.ValidationError as err: + raise ValueError( + f"Template '{self.template_id}' fails template_definition.json" + f" schema: {err.message}" + ) from err loop_vars = {"item"} for comp in self.components: diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json deleted file mode 100644 index 3c0eb0028f..0000000000 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/schema/template_definition.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "A2UI Template Definition", - "description": "Schema defining a parameterized A2UI declarative template authored in YAML with a nested layout tree and synthetic ID generation.", - "type": "object", - "required": ["version", "templateId", "parameters"], - "properties": { - "version": { - "type": "string", - "const": "0.1", - "description": "Specification version of the template definition format. Forced to '0.1'." - }, - "templateId": { - "type": "string", - "pattern": "^[A-Za-z][A-Za-z0-9_]*$", - "description": "Unique identifier and component tag name of the template." - }, - "description": { - "type": "string", - "description": "Human-readable description of what this template represents." - }, - "parameters": { - "type": "object", - "description": "Mapping of parameter names to their explicit A2UI type definitions.", - "additionalProperties": { - "$ref": "#/$defs/ParameterDefinition" - } - }, - "requiredParameters": { - "type": "array", - "items": {"type": "string"}, - "description": "Optional list of parameter names that are required when invoking the template." - }, - "layout": { - "$ref": "#/$defs/NestedTemplateNode", - "description": "Root component of the nested layout tree." - }, - "components": { - "type": "array", - "description": "Optional flattened component list for internal graph representation.", - "items": { - "$ref": "#/$defs/TemplateComponent" - } - }, - "sampleData": { - "type": "object", - "description": "Sample parameter values used for previewing and testing template inflation." - } - }, - "anyOf": [ - {"required": ["layout"]}, - {"required": ["components"]} - ], - "additionalProperties": false, - "$defs": { - "ParameterDefinition": { - "type": "object", - "required": ["type"], - "properties": { - "type": { - "type": "string", - "enum": [ - "string", - "number", - "integer", - "boolean", - "enum", - "object", - "array", - "child", - "children", - "action" - ], - "description": "A2UI semantic parameter type." - }, - "title": { - "type": "string", - "description": "Short human-readable title of the parameter." - }, - "description": { - "type": "string", - "description": "Detailed explanation of the parameter's purpose and usage." - }, - "default": { - "description": "Default fallback value if not specified." - }, - "values": { - "type": "array", - "items": {"type": "string"}, - "description": "Allowed string values when type is 'enum'." - }, - "items": { - "description": "Item type definition when type is 'array'. Can be an A2UI type name or nested ParameterDefinition." - }, - "properties": { - "type": "object", - "description": "Property definitions when type is 'object'.", - "additionalProperties": { - "$ref": "#/$defs/ParameterDefinition" - } - }, - "required": { - "anyOf": [ - { - "type": "boolean", - "description": "Whether this parameter is required when instantiating the template." - }, - { - "type": "array", - "items": {"type": "string"}, - "description": "List of required property names for object types." - } - ] - }, - "minimum": { - "type": "number", - "description": "Minimum value constraint for numeric parameters." - }, - "maximum": { - "type": "number", - "description": "Maximum value constraint for numeric parameters." - } - }, - "additionalProperties": false - }, - "ParamReference": { - "type": "object", - "required": ["param"], - "properties": { - "param": { - "type": "string", - "description": "Name or dot-separated path of the template parameter to substitute." - }, - "template": { - "type": "string", - "description": "Child template identifier when mapping an array parameter to child component instances." - }, - "default": { - "description": "Fallback value if the parameter path resolves to None." - } - }, - "additionalProperties": false - }, - "ConcatExpression": { - "type": "object", - "required": ["concat"], - "properties": { - "concat": { - "type": "array", - "description": "List of literal values and parameter references to concatenate into a string.", - "items": { - "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] - } - } - }, - "additionalProperties": false - }, - "FormatExpression": { - "type": "object", - "required": ["format"], - "properties": { - "format": { - "type": "string", - "description": "Format string containing {key} placeholders." - }, - "args": { - "type": "object", - "description": "Mapping of placeholder keys to values or parameter references.", - "additionalProperties": { - "anyOf": [{"type": "string"}, {"type": "number"}, {"$ref": "#/$defs/ParamReference"}] - } - } - }, - "additionalProperties": false - }, - "LoopDefinition": { - "type": "object", - "required": ["param"], - "anyOf": [ - {"required": ["template"]}, - {"required": ["item"]} - ], - "properties": { - "param": { - "type": "string", - "description": "Parameter name holding the list/array data to iterate over." - }, - "template": { - "type": "string", - "description": "Identifier of a named template to instantiate for each item." - }, - "item": { - "$ref": "#/$defs/NestedTemplateNode", - "description": "Self-contained inline component layout to instantiate for each item." - }, - "as": { - "type": "string", - "description": "Optional variable scope alias for accessing item fields in expressions." - } - }, - "additionalProperties": false - }, - "TemplateValue": { - "description": "A literal value, parameter substitution, or expression in a template component property.", - "anyOf": [ - {"type": "string"}, - {"type": "number"}, - {"type": "boolean"}, - {"type": "null"}, - {"$ref": "#/$defs/ParamReference"}, - {"$ref": "#/$defs/ConcatExpression"}, - {"$ref": "#/$defs/FormatExpression"}, - {"type": "array"}, - {"type": "object"} - ] - }, - "NestedTemplateNode": { - "type": "object", - "required": ["component"], - "properties": { - "id": { - "type": "string", - "description": "Optional explicit component ID within the template." - }, - "component": { - "type": "string", - "description": "Catalog component tag name." - }, - "child": { - "anyOf": [ - {"$ref": "#/$defs/NestedTemplateNode"}, - {"$ref": "#/$defs/TemplateValue"} - ] - }, - "children": { - "anyOf": [ - { - "type": "object", - "required": ["loop"], - "properties": { - "loop": {"$ref": "#/$defs/LoopDefinition"} - }, - "additionalProperties": false - }, - {"type": "array"}, - {"$ref": "#/$defs/ParamReference"}, - {"type": "string"} - ] - } - }, - "additionalProperties": { - "$ref": "#/$defs/TemplateValue" - } - }, - "TemplateComponent": { - "type": "object", - "required": ["id", "component"], - "properties": { - "id": { - "type": "string", - "description": "Unique component ID within the template." - }, - "component": { - "type": "string", - "description": "Catalog component tag name." - } - }, - "additionalProperties": { - "$ref": "#/$defs/TemplateValue" - } - } - } -} From 93e2ee3cf61d77815e7d4aeefe7f54b6d56d1d39 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 10:58:34 +0930 Subject: [PATCH 11/26] style(templates): add license headers to conformance YAML files, format, and fix relative doc link --- .../experimental/template/README.md | 8 +- .../schema/template_conformance_schema.json | 12 +- .../conformance/suites/01_substitution.yaml | 54 +++--- .../suites/02_static_expansion.yaml | 148 +++++++++------- .../conformance/suites/03_loop_unrolling.yaml | 164 ++++++++++-------- .../suites/04_parameter_validation.yaml | 32 +++- .../suites/05_message_interception.yaml | 134 +++++++------- .../suites/06_error_invariants.yaml | 44 +++-- 8 files changed, 340 insertions(+), 256 deletions(-) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md index 303f4dece0..81d8f2c70e 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/README.md @@ -3,13 +3,14 @@ The `a2ui.inference_formats.experimental.template` package provides server-side UI template expansion and synthetic catalog compilation for the A2UI Python Agent SDK. For the language-agnostic protocol specification, JSON schemas, and multi-platform design guidelines, see: -👉 **[`specification/proposals/templates/README.md`](../../../../../../specification/proposals/templates/README.md)** +👉 **[`specification/proposals/templates/README.md`](../../../../../../../../specification/proposals/templates/README.md)** --- ## Python API Reference ### 1. `StaticTemplate` + Represents a declarative, immutable UI template parsed from YAML or a dictionary. ```python @@ -23,9 +24,11 @@ single_template = StaticTemplate.from_yaml(yaml_string) ``` ### 2. `DynamicTemplate` + Extends templates with server-side execution. Supports two modes: #### A. Data-Binding Mode (`resolver + layout`) + Takes a lookup argument (e.g. `employeeId`), runs a resolver callback, and injects the result into a static YAML presentation layout: ```python @@ -46,6 +49,7 @@ dynamic_salary = DynamicTemplate( ``` #### B. Programmatic Render Mode (`render`) + Bypasses static layouts and runs native Python logic (loops, arithmetic, conditionals) returning a component AST: ```python @@ -73,6 +77,7 @@ server_template = DynamicTemplate( ``` ### 3. `TemplateProcessor` + The core synchronous template expansion and catalog generation engine. ```python @@ -96,6 +101,7 @@ expanded_components: A2UIComponentList = processor.expand_template( ``` ### 4. `TemplateInferenceFormat` + Integrates templates seamlessly with LLM prompting and parsing. ```python diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json index be01fc3da9..f23e5f5955 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json @@ -5,10 +5,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "name", - "action" - ], + "required": ["name", "action"], "properties": { "name": { "type": "string", @@ -46,10 +43,7 @@ "description": "Identifier of the template being expanded." }, "base_catalog": { - "type": [ - "object", - "null" - ], + "type": ["object", "null"], "description": "Optional base catalog schema provided to processor." }, "args": { @@ -75,4 +69,4 @@ } } } -} \ No newline at end of file +} diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml index b6c2990704..e0f4269741 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_exact_integer_preservation description: Verifies that exact token match preserves integer type without string coercion. @@ -22,13 +36,13 @@ value: ${tags} params: tags: - - admin - - developer - - tester + - admin + - developer + - tester expect: - - admin - - developer - - tester + - admin + - developer + - tester - name: test_exact_object_preservation description: Verifies that exact token match preserves nested dictionary structure. action: substitute_params @@ -93,16 +107,16 @@ value: ${teams.0.members.1.name} params: teams: - - members: - - name: First - - name: Second + - members: + - name: First + - name: Second expect: Second - name: test_format_expression_ast description: Verifies evaluating format expression dictionary with arguments. action: substitute_params args: value: - format: 'Progress: {current} of {total} ({pct}%)' + format: "Progress: {current} of {total} ({pct}%)" args: current: ${metrics.completed} total: ${metrics.total} @@ -111,7 +125,7 @@ metrics: completed: 15 total: 20 - expect: 'Progress: 15 of 20 (75%)' + expect: "Progress: 15 of 20 (75%)" - name: test_concat_expression_ast description: Verifies evaluating concat expression combining string literals and param references. @@ -119,15 +133,15 @@ args: value: concat: - - 'Status: ' - - param: state - - ' (' - - param: code - - ) + - "Status: " + - param: state + - " (" + - param: code + - ) params: state: OK code: 200 - expect: 'Status: OK (200)' + expect: "Status: OK (200)" - name: test_concat_with_none_omits_part description: Verifies that None or unresolved parts in concat do not insert 'None' strings. @@ -135,9 +149,9 @@ args: value: concat: - - Prefix- - - param: missing - - -Suffix + - Prefix- + - param: missing + - -Suffix params: {} expect: Prefix--Suffix - name: test_param_ref_with_default diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml index 65304f4bf0..4b5ee1dced 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml @@ -1,9 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_single_child_card_expansion description: Verifies expanding card container with single child and deterministic ID prefixing. action: expand_template template: - version: '0.1' + version: "0.1" templateId: SimpleCard parameters: title: @@ -18,17 +32,17 @@ args: title: Welcome expect: - - id: card_inst - component: Card - child: card_inst_child_text - - id: card_inst_child_text - component: Text - text: Welcome + - id: card_inst + component: Card + child: card_inst_child_text + - id: card_inst_child_text + component: Text + text: Welcome - name: test_default_param_values description: Verifies default value is assigned when argument is omitted. action: expand_template template: - version: '0.1' + version: "0.1" templateId: ProfileHeader parameters: name: @@ -39,31 +53,31 @@ layout: component: Column children: - - component: Text - text: ${name} - - component: Text - text: ${role} + - component: Text + text: ${name} + - component: Text + text: ${role} instance_id: prof_1 template_id: ProfileHeader args: name: Bob expect: - - id: prof_1 - component: Column - children: - - prof_1_child_0_text - - prof_1_child_1_text - - id: prof_1_child_0_text - component: Text - text: Bob - - id: prof_1_child_1_text - component: Text - text: Team Member + - id: prof_1 + component: Column + children: + - prof_1_child_0_text + - prof_1_child_1_text + - id: prof_1_child_0_text + component: Text + text: Bob + - id: prof_1_child_1_text + component: Text + text: Team Member - name: test_higher_order_child_slot description: Verifies passing a component ID into a child slot parameter. action: expand_template template: - version: '0.1' + version: "0.1" templateId: ContainerSlot parameters: content: @@ -76,15 +90,15 @@ args: content: custom_chart_comp_id expect: - - id: slot_inst - component: Card - child: custom_chart_comp_id + - id: slot_inst + component: Card + child: custom_chart_comp_id - name: test_unfilled_optional_child_slot_omitted description: Verifies that when an optional child slot parameter is omitted, the child property is omitted from output. action: expand_template template: - version: '0.1' + version: "0.1" templateId: OptionalChildCard parameters: optionalChild: @@ -97,13 +111,13 @@ template_id: OptionalChildCard args: {} expect: - - id: opt_card - component: Card + - id: opt_card + component: Card - name: test_higher_order_action_slot description: Verifies binding an Action object to a button action slot via parameter. action: expand_template template: - version: '0.1' + version: "0.1" templateId: ActionButton parameters: label: @@ -123,19 +137,19 @@ parameters: env: production expect: - - id: btn_inst - component: Button - text: Deploy - action: - name: triggerDeployment - parameters: - env: production + - id: btn_inst + component: Button + text: Deploy + action: + name: triggerDeployment + parameters: + env: production - name: test_data_binding_passthrough description: Verifies passing a client DataBinding dictionary through a parameter preserving dictionary structure. action: expand_template template: - version: '0.1' + version: "0.1" templateId: BoundMetric parameters: label: @@ -152,41 +166,41 @@ valueBinding: path: /system/cpu/usage expect: - - id: bound_txt - component: Text - text: - path: /system/cpu/usage + - id: bound_txt + component: Text + text: + path: /system/cpu/usage - name: test_nested_template_invocation description: Verifies a parent template layout referencing a child template expanding recursively. action: expand_template templates: - - version: '0.1' - templateId: Badge - parameters: - text: - type: string - layout: - component: Text - text: '[${text}]' - - version: '0.1' - templateId: BadgeCard - parameters: - tag: - type: string - layout: - component: Card - child: - component: Badge - text: ${tag} + - version: "0.1" + templateId: Badge + parameters: + text: + type: string + layout: + component: Text + text: "[${text}]" + - version: "0.1" + templateId: BadgeCard + parameters: + tag: + type: string + layout: + component: Card + child: + component: Badge + text: ${tag} instance_id: parent_inst template_id: BadgeCard args: tag: PRO expect: - - id: parent_inst - component: Card - child: parent_inst_child_badge - - id: parent_inst_child_badge - component: Text - text: '[PRO]' + - id: parent_inst + component: Card + child: parent_inst_child_badge + - id: parent_inst_child_badge + component: Text + text: "[PRO]" diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml index 369c94b3a1..a0b23a13c8 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml @@ -1,8 +1,22 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_inline_item_loop_unrolling description: Verifies inline component layout unrolling for each element of an array. action: expand_template template: - version: '0.1' + version: "0.1" templateId: MemberList parameters: members: @@ -16,82 +30,82 @@ item: component: Row children: - - component: Text - text: ${m.name} + - component: Text + text: ${m.name} instance_id: team_col template_id: MemberList args: members: - - name: Alice - - name: Bob + - name: Alice + - name: Bob expect: - - id: team_col - component: Column - children: - - team_col_item_0 - - team_col_item_1 - - id: team_col_item_0 - component: Row - children: - - team_col_item_0_child_0_text - - id: team_col_item_0_child_0_text - component: Text - text: Alice - - id: team_col_item_1 - component: Row - children: - - team_col_item_1_child_0_text - - id: team_col_item_1_child_0_text - component: Text - text: Bob + - id: team_col + component: Column + children: + - team_col_item_0 + - team_col_item_1 + - id: team_col_item_0 + component: Row + children: + - team_col_item_0_child_0_text + - id: team_col_item_0_child_0_text + component: Text + text: Alice + - id: team_col_item_1 + component: Row + children: + - team_col_item_1_child_0_text + - id: team_col_item_1_child_0_text + component: Text + text: Bob - name: test_named_template_loop_unrolling description: Verifies unrolling an array by invoking another registered template for each element. action: expand_template templates: - - version: '0.1' - templateId: TagBadge - parameters: - label: - type: string - layout: - component: Text - text: '#${label}' - - version: '0.1' - templateId: TagContainer - parameters: - tags: - type: array - layout: - component: Row - children: - loop: - param: tags - template: TagBadge + - version: "0.1" + templateId: TagBadge + parameters: + label: + type: string + layout: + component: Text + text: "#${label}" + - version: "0.1" + templateId: TagContainer + parameters: + tags: + type: array + layout: + component: Row + children: + loop: + param: tags + template: TagBadge instance_id: row_tags template_id: TagContainer args: tags: - - label: alpha - - label: beta + - label: alpha + - label: beta expect: - - id: row_tags - component: Row - children: - - row_tags_item_0 - - row_tags_item_1 - - id: row_tags_item_0 - component: Text - text: '#alpha' - - id: row_tags_item_1 - component: Text - text: '#beta' + - id: row_tags + component: Row + children: + - row_tags_item_0 + - row_tags_item_1 + - id: row_tags_item_0 + component: Text + text: "#alpha" + - id: row_tags_item_1 + component: Text + text: "#beta" - name: test_empty_loop_array_emits_empty_children description: Verifies that an empty array parameter unrolls cleanly to empty children list. action: expand_template template: - version: '0.1' + version: "0.1" templateId: EmptyListChecker parameters: items: @@ -109,15 +123,15 @@ args: items: [] expect: - - id: empty_col - component: Column - children: [] + - id: empty_col + component: Column + children: [] - name: test_primitive_list_iteration description: Verifies iterating over primitive strings in an array using item.value fallback. action: expand_template template: - version: '0.1' + version: "0.1" templateId: ColorChips parameters: colors: @@ -135,26 +149,26 @@ template_id: ColorChips args: colors: - - Crimson - - Azure + - Crimson + - Azure expect: - - id: chip_row - component: Row - children: - - chip_row_item_0 - - chip_row_item_1 - - id: chip_row_item_0 - component: Text - text: Crimson - - id: chip_row_item_1 - component: Text - text: Azure + - id: chip_row + component: Row + children: + - chip_row_item_0 + - chip_row_item_1 + - id: chip_row_item_0 + component: Text + text: Crimson + - id: chip_row_item_1 + component: Text + text: Azure - name: test_non_array_loop_parameter_raises_error description: Verifies that passing a non-array string value to a loop raises a clear ValidationError. action: expand_template template: - version: '0.1' + version: "0.1" templateId: FailingLoop parameters: items: diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml index 86f57ff0ec..b1fb7a1e12 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml @@ -1,16 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_parameter_type_validation_string_to_integer_fails description: Verifies that passing a string to an integer parameter raises a validation error. action: expand_template template: - version: '0.1' + version: "0.1" templateId: IntegerParamCheck parameters: count: type: integer layout: component: Text - text: 'Count: ${count}' + text: "Count: ${count}" instance_id: int_fail template_id: IntegerParamCheck args: @@ -23,18 +37,18 @@ validation error. action: expand_template template: - version: '0.1' + version: "0.1" templateId: EnumParamCheck parameters: status: type: enum values: - - ACTIVE - - SUSPENDED - - PENDING + - ACTIVE + - SUSPENDED + - PENDING layout: component: Text - text: 'Status: ${status}' + text: "Status: ${status}" instance_id: enum_fail template_id: EnumParamCheck args: @@ -47,7 +61,7 @@ an error. action: expand_template template: - version: '0.1' + version: "0.1" templateId: RequiredParamCheck parameters: mandatory: @@ -66,7 +80,7 @@ error. action: validate_template template: - version: '0.2' + version: "0.2" templateId: FutureVersionTemplate parameters: {} layout: diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml index 4a53961267..b19880d60f 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml @@ -1,9 +1,23 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_create_surface_envelope_unwrapping description: Verifies that createSurface message wraps template instance and unwraps into primitive components. action: process_message template: - version: '0.1' + version: "0.1" templateId: SimpleCard parameters: title: @@ -18,26 +32,26 @@ surfaceId: main catalogId: https://a2ui.org/custom.json components: - - id: root - component: SimpleCard - title: Unwrapped + - id: root + component: SimpleCard + title: Unwrapped expect: createSurface: surfaceId: main catalogId: https://a2ui.org/custom.json components: - - id: root - component: Card - child: root_child_text - - id: root_child_text - component: Text - text: Unwrapped + - id: root + component: Card + child: root_child_text + - id: root_child_text + component: Text + text: Unwrapped - name: test_update_components_envelope_unwrapping description: Verifies that updateComponents message with template instance expands correctly. action: process_message template: - version: '0.1' + version: "0.1" templateId: SimpleCard parameters: title: @@ -51,25 +65,25 @@ updateComponents: surfaceId: main components: - - id: card_2 - component: SimpleCard - title: Updated + - id: card_2 + component: SimpleCard + title: Updated expect: updateComponents: surfaceId: main components: - - id: card_2 - component: Card - child: card_2_child_text - - id: card_2_child_text - component: Text - text: Updated + - id: card_2 + component: Card + child: card_2_child_text + - id: card_2_child_text + component: Text + text: Updated - name: test_mixed_primitives_and_templates_in_message description: Verifies that non-template primitive components pass through untouched alongside expanded templates. action: process_message template: - version: '0.1' + version: "0.1" templateId: SimpleCard parameters: title: @@ -83,31 +97,31 @@ updateComponents: surfaceId: main components: - - id: div_1 - component: Divider - axis: horizontal - - id: card_mixed - component: SimpleCard - title: Mixed Content + - id: div_1 + component: Divider + axis: horizontal + - id: card_mixed + component: SimpleCard + title: Mixed Content expect: updateComponents: surfaceId: main components: - - id: div_1 - component: Divider - axis: horizontal - - id: card_mixed - component: Card - child: card_mixed_child_text - - id: card_mixed_child_text - component: Text - text: Mixed Content + - id: div_1 + component: Divider + axis: horizontal + - id: card_mixed + component: Card + child: card_mixed_child_text + - id: card_mixed_child_text + component: Text + text: Mixed Content - name: test_batch_messages_stream_passthrough description: Verifies processing a list of messages passing dataModel updates through unaltered. action: process_message template: - version: '0.1' + version: "0.1" templateId: SimpleCard parameters: title: @@ -118,27 +132,27 @@ component: Text text: ${title} message: - - updateDataModel: - path: /session/user - value: - name: Alice - - updateComponents: - surfaceId: main - components: - - id: batch_c - component: SimpleCard - title: Batch + - updateDataModel: + path: /session/user + value: + name: Alice + - updateComponents: + surfaceId: main + components: + - id: batch_c + component: SimpleCard + title: Batch expect: - - updateDataModel: - path: /session/user - value: - name: Alice - - updateComponents: - surfaceId: main - components: - - id: batch_c - component: Card - child: batch_c_child_text - - id: batch_c_child_text - component: Text - text: Batch + - updateDataModel: + path: /session/user + value: + name: Alice + - updateComponents: + surfaceId: main + components: + - id: batch_c + component: Card + child: batch_c_child_text + - id: batch_c_child_text + component: Text + text: Batch diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml index 6fbd0b7b20..2e1631a375 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + - name: test_unregistered_template_raises_error description: Verifies expanding an unregistered template ID raises a clear ValueError. action: expand_template @@ -13,16 +27,16 @@ -> A. action: expand_template templates: - - version: '0.1' - templateId: NodeA - parameters: {} - layout: - component: NodeB - - version: '0.1' - templateId: NodeB - parameters: {} - layout: - component: NodeA + - version: "0.1" + templateId: NodeA + parameters: {} + layout: + component: NodeB + - version: "0.1" + templateId: NodeB + parameters: {} + layout: + component: NodeA instance_id: root_circ template_id: NodeA args: {} @@ -34,11 +48,11 @@ the cycle guard. action: expand_template templates: - - version: '0.1' - templateId: SelfLoop - parameters: {} - layout: - component: SelfLoop + - version: "0.1" + templateId: SelfLoop + parameters: {} + layout: + component: SelfLoop instance_id: root_self template_id: SelfLoop args: {} From 45e68cdeced722c5383230a3c2144d7c6a234389 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:22:13 +0930 Subject: [PATCH 12/26] feat(python-sdk): require explicit catalogs and support name, id, catalogs, and imports in templates --- .../experimental/template/format.py | 2 +- .../experimental/template/models.py | 102 +++++-- .../experimental/template/processor.py | 253 +++++++++++++++--- .../schema/template_conformance_schema.json | 3 + .../suites/02_static_expansion.yaml | 24 ++ .../conformance/suites/03_loop_unrolling.yaml | 18 ++ .../suites/04_parameter_validation.yaml | 13 + .../suites/05_message_interception.yaml | 12 + .../suites/06_error_invariants.yaml | 9 + .../experimental/template/test_format.py | 4 +- .../experimental/template/test_processor.py | 198 ++++++++++---- .../template/test_template_conformance.py | 26 +- 12 files changed, 549 insertions(+), 115 deletions(-) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py index 725d217001..e1a91e2561 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/format.py @@ -206,7 +206,7 @@ def __init__( # 2. Initialize TemplateProcessor self.processor = TemplateProcessor( templates=self.templates, - base_catalog=self.base_catalog, + catalogs=self.base_catalog, version=self.version, ) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py index 23841222c3..99311c0ed7 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -24,7 +24,7 @@ from pathlib import Path import re from dataclasses import dataclass, field -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import jsonschema import yaml @@ -331,9 +331,12 @@ class TemplateComponent: properties: Dict[str, Any] = field(default_factory=dict) child: Optional[Union[str, ParamRef, Dict[str, Any]]] = None children: Optional[Union[List[Any], ParamRef, TemplateLoop, Dict[str, Any]]] = None + catalog_id: Optional[str] = None def to_dict(self) -> Dict[str, Any]: res: Dict[str, Any] = {"id": self.id, "component": self.component} + if self.catalog_id is not None: + res["catalogId"] = self.catalog_id if self.child is not None: res["child"] = ( self.child.to_dict() if isinstance(self.child, ParamRef) else self.child @@ -355,11 +358,13 @@ def from_dict(cls, data: Dict[str, Any]) -> TemplateComponent: data_copy = dict(data) c_id = data_copy.pop("id") c_type = data_copy.pop("component") + c_cat = data_copy.pop("catalogId", None) or data_copy.pop("catalog_id", None) child = data_copy.pop("child", None) children = data_copy.pop("children", None) return cls( id=c_id, component=c_type, + catalog_id=c_cat, properties=data_copy, child=child, children=children, @@ -508,11 +513,23 @@ def normalize_a2ui_type_to_jsonschema(meta: Any) -> Any: class BaseTemplate: """Base class for all templates.""" - template_id: str + name: str = "" + catalogs: List[str] = field(default_factory=list) + id: Optional[str] = None + imports: Union[List[str], Dict[str, str]] = field(default_factory=list) version: str = "0.1" description: Optional[str] = None sample_data: Optional[Dict[str, Any]] = None is_dynamic: bool = False + template_id: str = "" + + def __post_init__(self): + if not self.name and self.template_id: + self.name = self.template_id + elif not self.template_id and self.name: + self.template_id = self.name + if isinstance(self.catalogs, str): + self.catalogs = [self.catalogs] @dataclass @@ -528,21 +545,32 @@ class StaticTemplate(BaseTemplate): @classmethod def from_dict(cls, data: Dict[str, Any]) -> StaticTemplate: """Constructs a StaticTemplate instance from a dictionary payload.""" - template_id = data.get("templateId") or data.get("template_id") - if not template_id: - raise ValueError("Template dictionary must contain 'templateId'.") + name = data.get("name") or data.get("templateId") or data.get("template_id") + if not name: + raise ValueError( + "Template dictionary must contain 'name' (or 'templateId')." + ) version = data.get("version") if not version: - raise ValueError( - f"Template '{template_id}' missing required 'version' attribute." - ) + raise ValueError(f"Template '{name}' missing required 'version' attribute.") if version != "0.1": raise ValueError( f"Unsupported template version '{version}'. Currently only '0.1' is" " supported." ) + catalogs_raw = data.get("catalogs", []) + if isinstance(catalogs_raw, str): + catalogs = [catalogs_raw] + elif isinstance(catalogs_raw, (list, tuple)): + catalogs = list(catalogs_raw) + else: + catalogs = [] + + id_val = data.get("id") + imports = data.get("imports", []) + raw_params = data.get("parameters", {}) parsed_params: Dict[str, Union[Param, Dict[str, Any]]] = {} for k, v in raw_params.items(): @@ -558,11 +586,15 @@ def from_dict(cls, data: Dict[str, Any]) -> StaticTemplate: components = list(data["components"]) else: raise ValueError( - f"Template '{template_id}' must declare a 'layout' tree definition." + f"Template '{name}' must declare a 'layout' tree definition." ) instance = cls( - template_id=template_id, + name=name, + catalogs=catalogs, + id=id_val, + imports=imports, + template_id=name, version=version, parameters=parsed_params, components=components, @@ -612,9 +644,15 @@ def to_dict(self) -> Dict[str, Any]: res: Dict[str, Any] = { "version": self.version, - "templateId": self.template_id, + "name": self.name, + "templateId": self.name, + "catalogs": self.catalogs, "parameters": params_dict, } + if self.id is not None: + res["id"] = self.id + if self.imports: + res["imports"] = self.imports if self.raw_layout is not None: res["layout"] = self.raw_layout else: @@ -744,7 +782,10 @@ class DynamicTemplate(BaseTemplate): def __init__( self, - template_id: str, + name: Optional[str] = None, + catalogs: Optional[Union[List[str], str]] = None, + id: Optional[str] = None, + imports: Optional[Union[List[str], Dict[str, str]]] = None, render: Optional[Callable[..., Any]] = None, resolver: Optional[Callable[..., Any]] = None, layout: Optional[Union[StaticTemplate, str, Dict[str, Any]]] = None, @@ -753,18 +794,13 @@ def __init__( sample_data: Optional[Dict[str, Any]] = None, render_fn: Optional[Callable[..., Any]] = None, version: str = "0.1", + template_id: Optional[str] = None, ): - super().__init__( - template_id=template_id, - version=version, - description=description, - sample_data=sample_data, - is_dynamic=True, - ) - self.render_fn = render or render_fn - self.resolver = resolver - self.layout: Optional[StaticTemplate] = None + template_name = name or template_id + if not template_name: + raise ValueError("DynamicTemplate must provide 'name' (or 'template_id').") + self.layout: Optional[StaticTemplate] = None if layout is not None: if isinstance(layout, str): if os.path.exists(layout): @@ -776,10 +812,30 @@ def __init__( else: self.layout = layout + catalogs_list: List[str] = [] + if catalogs is not None: + catalogs_list = [catalogs] if isinstance(catalogs, str) else list(catalogs) + elif self.layout is not None and self.layout.catalogs: + catalogs_list = list(self.layout.catalogs) + + super().__init__( + name=template_name, + catalogs=catalogs_list, + id=id, + imports=imports or [], + template_id=template_name, + version=version, + description=description, + sample_data=sample_data, + is_dynamic=True, + ) + self.render_fn = render or render_fn + self.resolver = resolver + target_fn = self.render_fn or self.resolver if target_fn is None and self.layout is None: raise ValueError( - f"DynamicTemplate '{template_id}' must provide either a 'render'" + f"DynamicTemplate '{template_name}' must provide either a 'render'" " function or a ('resolver', 'layout') pair." ) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py index 172dd37e22..b3aaea83c2 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py @@ -194,6 +194,11 @@ def __init__( templates: Sequence[ Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] ], + catalogs: Optional[ + Union[ + CatalogSchema, Sequence[CatalogSchema], Mapping[str, CatalogSchema], Any + ] + ] = None, base_catalog: Optional[Union[CatalogSchema, Any]] = None, version: str = "v0.9.1", ): @@ -201,29 +206,89 @@ def __init__( Args: templates: List of registered Template definitions. - base_catalog: Optional base catalog schema dict or A2uiCatalog instance. - If omitted, the processor operates independently without catalog assumptions. + catalogs: Mandatory catalog schema(s) or A2uiCatalog instance(s) that templates + are validated and resolved against. No default to basic catalog is assumed. + base_catalog: Backwards-compatible alias for catalogs. version: Target A2UI protocol version ("v0.9", "v0.9.1", or "v1.0"). """ - self.templates: Dict[ - TemplateId, Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] - ] = {t.template_id: t for t in templates} + raw_catalogs = catalogs if catalogs is not None else base_catalog + if raw_catalogs is None: + raise ValueError( + "TemplateProcessor requires explicit catalog(s) to be provided via" + " 'catalogs'. No defaults to the basic catalog are assumed." + ) + self.version = version - self.base_catalog: Optional[CatalogSchema] = None - self.base_catalog_id: Optional[str] = None + self.catalogs: Dict[str, CatalogSchema] = {} - if base_catalog is not None: - if hasattr(base_catalog, "catalog_schema") and isinstance( - base_catalog.catalog_schema, dict + def _extract_catalog(c: Any) -> Tuple[str, CatalogSchema]: + if hasattr(c, "catalog_schema") and isinstance(c.catalog_schema, dict): + schema = c.catalog_schema + elif isinstance(c, dict): + schema = c + else: + raise ValueError(f"Invalid catalog object: {type(c)}") + c_id = ( + schema.get("catalogId") + or schema.get("$id") + or schema.get("id") + or "default" + ) + return str(c_id), schema + + def _register_catalog(c_id: str, schema: CatalogSchema) -> None: + self.catalogs[c_id] = schema + if "v0_9/catalogs/basic/catalog.json" in c_id: + self.catalogs[c_id.replace("v0_9/", "v0_9_1/")] = schema + elif "v0_9_1/catalogs/basic/catalog.json" in c_id: + self.catalogs[c_id.replace("v0_9_1/", "v0_9/")] = schema + + if isinstance(raw_catalogs, (list, tuple, set)): + for item in raw_catalogs: + c_id, s = _extract_catalog(item) + _register_catalog(c_id, s) + elif isinstance(raw_catalogs, dict): + if ( + "components" in raw_catalogs + or "$id" in raw_catalogs + or "catalogId" in raw_catalogs ): - self.base_catalog = base_catalog.catalog_schema - elif isinstance(base_catalog, dict): - self.base_catalog = base_catalog + c_id, s = _extract_catalog(raw_catalogs) + _register_catalog(c_id, s) + else: + for k, v in raw_catalogs.items(): + _, s = _extract_catalog(v) + _register_catalog(str(k), s) + else: + c_id, s = _extract_catalog(raw_catalogs) + _register_catalog(c_id, s) + + if not self.catalogs: + raise ValueError( + "TemplateProcessor requires at least one catalog. Empty catalog" + " collection provided." + ) + + self.base_catalog_id = next(iter(self.catalogs.keys())) + self.base_catalog = self.catalogs[self.base_catalog_id] - if self.base_catalog is not None: - self.base_catalog_id = self.base_catalog.get( - "$id" - ) or self.base_catalog.get("id") + self.templates: Dict[ + TemplateId, Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] + ] = {} + self.templates_by_id: Dict[ + str, Union[BaseTemplate, Template, StaticTemplate, DynamicTemplate] + ] = {} + + for t in templates: + t_name = getattr(t, "name", None) or getattr(t, "template_id", None) + if not t_name: + continue + self.templates[t_name] = t + self.templates[t.template_id] = t + t_id = getattr(t, "id", None) + if t_id: + self.templates_by_id[t_id] = t + self.templates[t_id] = t self._validate_templates() @@ -233,12 +298,11 @@ def generate_inference_catalog( ) -> CatalogSchema: """Generates a synthetic JSON catalog containing allowed primitives and all registered templates.""" if allowed_primitives is None: - if self.base_catalog is not None: - allowed_primitives = list( - self.base_catalog.get("components", {}).keys() - ) - else: - allowed_primitives = [] + allowed_primitives = [] + for cat in self.catalogs.values(): + for comp_k in cat.get("components", {}).keys(): + if comp_k not in allowed_primitives: + allowed_primitives.append(comp_k) catalog_id = ( self.base_catalog_id @@ -270,11 +334,14 @@ def generate_inference_catalog( }, } - # 1. Copy over allowed primitives from the base catalog if present - if self.base_catalog is not None: - base_components = self.base_catalog.get("components", {}) + # 1. Copy over allowed primitives from registered catalogs + for cat in self.catalogs.values(): + base_components = cat.get("components", {}) for prim in allowed_primitives: - if prim in base_components: + if ( + prim in base_components + and prim not in synthetic_catalog["components"] + ): synthetic_catalog["components"][prim] = base_components[prim] synthetic_catalog["$defs"]["anyComponent"]["oneOf"].append( {"$ref": f"#/components/{prim}"} @@ -287,7 +354,13 @@ def generate_inference_catalog( else "https://a2ui.org/specification/v1_0/common_types.json" ) - for t_id, template in self.templates.items(): + seen_templates = set() + for template in self.templates.values(): + t_id = getattr(template, "name", template.template_id) + if t_id in seen_templates: + continue + seen_templates.add(t_id) + param_required = [] properties_dict: Dict[str, Any] = {"component": {"const": t_id}} @@ -710,7 +783,11 @@ def _expand_static_layout( sub_params[as_var] = item_dict inline_template = StaticTemplate( + name=f"{template_id}_inline_{idx}", template_id=f"{template_id}_inline_{idx}", + catalogs=getattr( + layout, "catalogs", list(self.catalogs.keys()) + ), parameters={}, components=item_comps, ) @@ -769,7 +846,20 @@ def _expand_static_layout( ) final_list.extend(nested_expanded) else: - final_list.append(c) + comp_out = dict(c) + if "0.9" in self.version: + comp_out.pop("catalogId", None) + elif "1.0" in self.version: + if "catalogId" not in comp_out: + matching = [ + cat + for cat in getattr(layout, "catalogs", []) + if c_type + in self.catalogs.get(cat, {}).get("components", {}) + ] + if matching and matching[0] != self.base_catalog_id: + comp_out["catalogId"] = matching[0] + final_list.append(comp_out) return final_list @@ -790,7 +880,7 @@ def process_message( payload = dict(msg_copy[envelope_key]) if ( envelope_key == "createSurface" - and "catalogId" in payload + and "catalogId" not in payload and self.base_catalog_id is not None ): payload["catalogId"] = self.base_catalog_id @@ -889,19 +979,104 @@ def _promote_parameter(self, p_meta: Dict[str, Any]) -> JSONSchemaDict: return dict(p_meta) def _validate_templates(self) -> None: - """Validates all registered templates.""" - for t_id, template in self.templates.items(): + """Validates all registered templates against declared catalogs and component references.""" + for t_key, template in list(self.templates.items()): + t_name = getattr(template, "name", template.template_id) + if t_key != t_name: + continue + + # 1. Resolve declared catalogs on template + t_catalogs = getattr(template, "catalogs", []) + if isinstance(t_catalogs, str): + t_catalogs = [t_catalogs] + template.catalogs = t_catalogs + + if not t_catalogs: + if len(self.catalogs) == 1: + t_catalogs = list(self.catalogs.keys()) + template.catalogs = t_catalogs + else: + raise ValueError( + f"Template '{t_name}' must declare which catalog(s) it is" + " written against via 'catalogs'. Registered catalogs:" + f" {list(self.catalogs.keys())}" + ) + + for c_uri in t_catalogs: + if c_uri not in self.catalogs: + raise ValueError( + f"Template '{t_name}' declares catalog '{c_uri}', which is not" + " registered in TemplateProcessor. Available catalogs:" + f" {list(self.catalogs.keys())}" + ) + + # In v0.9, a template cannot span multiple catalogs + if "0.9" in self.version and len(t_catalogs) > 1: + raise ValueError( + f"Template '{t_name}' declares multiple catalogs {t_catalogs}, but" + f" A2UI {self.version} only supports a single catalog." + ) + + # 2. Validate template definition schema if static if isinstance(template, StaticTemplate): template.validate_definition() - for comp in template.components: - comp_dict = ( - comp.to_dict() if isinstance(comp, TemplateComponent) else comp + + # 3. Validate components in layout + layout = getattr(template, "layout", None) or template + comps = getattr(layout, "components", []) + for comp in comps: + comp_dict = ( + comp.to_dict() if isinstance(comp, TemplateComponent) else comp + ) + comp_type = comp_dict.get("component") + if not comp_type: + raise ValueError( + f"Component in template '{t_name}' is missing 'component' type." ) - comp_type = comp_dict.get("component") - if not comp_type: + + # Check if component is a sub-template invocation + if ( + comp_type in self.templates + or comp_type in self.templates_by_id + or comp_type in getattr(template, "imports", []) + ): + continue + + # Otherwise it must be a primitive in the declared catalogs + matching_cats = [ + cat_uri + for cat_uri in t_catalogs + if comp_type in self.catalogs[cat_uri].get("components", {}) + ] + + explicit_cat = comp_dict.get("catalogId") or getattr( + comp, "catalog_id", None + ) + if explicit_cat: + if explicit_cat not in t_catalogs: + raise ValueError( + f"Component '{comp_type}' in template '{t_name}' specifies" + f" catalogId '{explicit_cat}', which is not in template's" + f" declared catalogs: {t_catalogs}" + ) + if comp_type not in self.catalogs[explicit_cat].get( + "components", {} + ): + raise ValueError( + f"Component '{comp_type}' not found in catalog" + f" '{explicit_cat}'" + ) + else: + if len(matching_cats) == 0: + raise ValueError( + f"Component '{comp_type}' in template '{t_name}' was not" + f" found in any declared catalog: {t_catalogs}" + ) + elif len(matching_cats) > 1: raise ValueError( - f"Component in template '{t_id}' is missing 'component'" - " type." + f"Component '{comp_type}' in template '{t_name}' is" + " ambiguous across multiple declared catalogs:" + f" {matching_cats}. Specify 'catalogId' on the component." ) def validate_templates(self) -> None: diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json index f23e5f5955..d68f8a11ac 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/schema/template_conformance_schema.json @@ -46,6 +46,9 @@ "type": ["object", "null"], "description": "Optional base catalog schema provided to processor." }, + "catalogs": { + "description": "Optional catalogs schema provided to processor." + }, "args": { "description": "Arguments/parameters passed to the action." }, diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml index 4b5ee1dced..33fb8a0295 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/02_static_expansion.yaml @@ -27,6 +27,9 @@ child: component: Text text: ${title} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SimpleCard instance_id: card_inst template_id: SimpleCard args: @@ -57,6 +60,9 @@ text: ${name} - component: Text text: ${role} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: ProfileHeader instance_id: prof_1 template_id: ProfileHeader args: @@ -85,6 +91,9 @@ layout: component: Card child: ${content} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: ContainerSlot instance_id: slot_inst template_id: ContainerSlot args: @@ -107,6 +116,9 @@ layout: component: Card child: ${optionalChild} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: OptionalChildCard instance_id: opt_card template_id: OptionalChildCard args: {} @@ -128,6 +140,9 @@ component: Button text: ${label} action: ${onPress} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: ActionButton instance_id: btn_inst template_id: ActionButton args: @@ -159,6 +174,9 @@ layout: component: Text text: ${valueBinding} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: BoundMetric instance_id: bound_txt template_id: BoundMetric args: @@ -183,6 +201,9 @@ layout: component: Text text: "[${text}]" + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: Badge - version: "0.1" templateId: BadgeCard parameters: @@ -193,6 +214,9 @@ child: component: Badge text: ${tag} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: BadgeCard instance_id: parent_inst template_id: BadgeCard args: diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml index a0b23a13c8..b8c353925e 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/03_loop_unrolling.yaml @@ -32,6 +32,9 @@ children: - component: Text text: ${m.name} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: MemberList instance_id: team_col template_id: MemberList args: @@ -71,6 +74,9 @@ layout: component: Text text: "#${label}" + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: TagBadge - version: "0.1" templateId: TagContainer parameters: @@ -82,6 +88,9 @@ loop: param: tags template: TagBadge + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: TagContainer instance_id: row_tags template_id: TagContainer args: @@ -118,6 +127,9 @@ item: component: Text text: Item + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: EmptyListChecker instance_id: empty_col template_id: EmptyListChecker args: @@ -145,6 +157,9 @@ item: component: Text text: ${c.value} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: ColorChips instance_id: chip_row template_id: ColorChips args: @@ -181,6 +196,9 @@ item: component: Text text: Fail + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: FailingLoop instance_id: fail_inst template_id: FailingLoop args: diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml index b1fb7a1e12..8b309e4375 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/04_parameter_validation.yaml @@ -25,6 +25,9 @@ layout: component: Text text: "Count: ${count}" + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: IntegerParamCheck instance_id: int_fail template_id: IntegerParamCheck args: @@ -49,6 +52,9 @@ layout: component: Text text: "Status: ${status}" + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: EnumParamCheck instance_id: enum_fail template_id: EnumParamCheck args: @@ -69,6 +75,9 @@ layout: component: Text text: ${mandatory} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: RequiredParamCheck instance_id: req_fail template_id: RequiredParamCheck args: {} @@ -86,6 +95,9 @@ layout: component: Text text: Future + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: FutureVersionTemplate expect_error: category: ValidationError message: Unsupported template version '0.2' @@ -98,6 +110,7 @@ layout: component: Text text: No Version + name: MissingVersionTemplate expect_error: category: ValidationError message: missing required 'version' attribute diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml index b19880d60f..23fe2d0e44 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/05_message_interception.yaml @@ -27,6 +27,9 @@ child: component: Text text: ${title} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SimpleCard message: createSurface: surfaceId: main @@ -61,6 +64,9 @@ child: component: Text text: ${title} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SimpleCard message: updateComponents: surfaceId: main @@ -93,6 +99,9 @@ child: component: Text text: ${title} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SimpleCard message: updateComponents: surfaceId: main @@ -131,6 +140,9 @@ child: component: Text text: ${title} + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SimpleCard message: - updateDataModel: path: /session/user diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml index 2e1631a375..c8bb843bec 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/06_error_invariants.yaml @@ -32,11 +32,17 @@ parameters: {} layout: component: NodeB + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: NodeA - version: "0.1" templateId: NodeB parameters: {} layout: component: NodeA + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: NodeB instance_id: root_circ template_id: NodeA args: {} @@ -53,6 +59,9 @@ parameters: {} layout: component: SelfLoop + catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json + name: SelfLoop instance_id: root_self template_id: SelfLoop args: {} diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py index 3449ed9d5f..e0d7c63c4a 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_format.py @@ -24,7 +24,9 @@ def test_template_inference_format_end_to_end(): """Verifies that TemplateInferenceFormat wraps ExpressFormat, generates prompts, and unrolls streaming chunks.""" yaml_tmpl = """ version: "0.1" -templateId: UserProfile +name: UserProfile +catalogs: + - "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" parameters: userId: {type: string} userName: {type: string} diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py index 1161c76aae..812ccfb3fb 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py @@ -15,7 +15,9 @@ """Unit tests for A2UI Template Processor (processor.py) focusing on Python runtime execution and typing.""" import asyncio -from typing import Any, Dict, List +import json +from pathlib import Path +from typing import Any, Dict import pytest from a2ui.inference_formats.experimental.template import ( @@ -27,24 +29,33 @@ ComponentId, ParamPath, TemplateParams, - TemplateNodeDict, - A2UIComponent, - A2UIComponentList, - A2UIMessage, - CatalogSchema, - JSONSchemaDict, ) from a2ui.inference_formats.experimental.template.processor import ( _resolve_param_path, _substitute_params, ) +BASIC_CATALOG_PATH = ( + Path(__file__).resolve().parents[7] + / "specification" + / "v0_9_1" + / "catalogs" + / "basic" + / "catalog.json" +) +BASIC_CATALOG: Dict[str, Any] = {} +if BASIC_CATALOG_PATH.is_file(): + with open(BASIC_CATALOG_PATH, "r", encoding="utf-8") as f: + BASIC_CATALOG = json.load(f) + def test_dynamic_template_with_resolver(): """Verifies Mode B dynamic template where a native Python resolver fetches data and binds it to a static layout.""" layout_yaml = """ version: "0.1" -templateId: LiveStockCard +name: LiveStockCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" parameters: ticker: {type: string} price: {type: number, required: false} @@ -72,13 +83,15 @@ async def fetch_stock_data(ticker: str) -> Dict[str, Any]: return mock_db.get(ticker, {"price": 0.0, "changePct": "0.0"}) tmpl = DynamicTemplate( + name="LiveStockCard", template_id="LiveStockCard", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], layout=layout_yaml, resolver=fetch_stock_data, description="Displays live stock pricing.", ) - processor = TemplateProcessor(templates=[tmpl]) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) expanded = processor.expand_template( "stock_goog", "LiveStockCard", {"ticker": "GOOG"} ) @@ -115,12 +128,14 @@ def render_payroll(department: str = "Engineering", includeBonus: bool = True): } tmpl = DynamicTemplate( + name="PayrollSummary", template_id="PayrollSummary", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], render=render_payroll, description="Dynamic payroll summary calculation.", ) - processor = TemplateProcessor(templates=[tmpl]) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) expanded = processor.expand_template( "payroll_root", "PayrollSummary", {"department": "AI Research"} ) @@ -135,8 +150,13 @@ def test_programmatic_dynamic_template_extra_params_filtered(): def render_card(title: str): return {"component": "Text", "text": title} - tmpl = DynamicTemplate(template_id="SimpleCard", render=render_card) - processor = TemplateProcessor(templates=[tmpl]) + tmpl = DynamicTemplate( + name="SimpleCard", + template_id="SimpleCard", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + render=render_card, + ) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) expanded = processor.expand_template( "inst", @@ -151,7 +171,9 @@ def test_dynamic_template_resolve_non_dict_fallback(): """Verifies graceful fallback when a resolver returns a non-dict value.""" layout_yaml = """ version: "0.1" -templateId: NonDictLayout +name: NonDictLayout +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" parameters: paramA: {type: string} layout: @@ -163,11 +185,13 @@ def bad_resolver(paramA: str): return "not a dict" tmpl = DynamicTemplate( + name="NonDictLayout", template_id="NonDictLayout", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], layout=layout_yaml, resolver=bad_resolver, ) - processor = TemplateProcessor(templates=[tmpl]) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) expanded = processor.expand_template("root", "NonDictLayout", {"paramA": "safe"}) assert len(expanded) == 1 assert expanded[0]["text"] == "safe" @@ -177,7 +201,9 @@ def test_dynamic_template_loop_custom_as_variable(): """Verifies dynamic template expansion utilizing custom loop variable naming.""" layout_yaml = """ version: "0.1" -templateId: CustomAsList +name: CustomAsList +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" parameters: items: {type: array} layout: @@ -191,7 +217,7 @@ def test_dynamic_template_loop_custom_as_variable(): text: ${item_var.label} """ tmpl = StaticTemplate.from_yaml(layout_yaml) - processor = TemplateProcessor(templates=[tmpl]) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) expanded = processor.expand_template( "root", "CustomAsList", @@ -202,33 +228,17 @@ def test_dynamic_template_loop_custom_as_variable(): assert "Second" in texts -def test_template_processor_completely_optional_base_catalog(): - """Verifies that TemplateProcessor works with base_catalog=None.""" - layout_yaml = """ -version: "0.1" -templateId: StandaloneTemplate -parameters: - title: {type: string} -layout: - component: Text - text: ${title} -""" - tmpl = StaticTemplate.from_yaml(layout_yaml) - processor = TemplateProcessor(templates=[tmpl], base_catalog=None) - - assert processor.base_catalog is None - assert processor.base_catalog_id is None - - cat = processor.generate_inference_catalog() - assert "StandaloneTemplate" in cat["components"] - assert "Card" not in cat["components"] - assert "Text" not in cat["components"] - - expanded = processor.expand_template( - "inst_1", "StandaloneTemplate", {"title": "Autonomous"} +def test_template_processor_requires_catalogs(): + """Verifies that TemplateProcessor requires explicit catalogs and rejects default assumptions.""" + t = StaticTemplate( + name="StandaloneTemplate", + template_id="StandaloneTemplate", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + parameters={}, + components=[], ) - assert len(expanded) == 1 - assert expanded[0]["text"] == "Autonomous" + with pytest.raises(ValueError, match="TemplateProcessor requires explicit catalog"): + TemplateProcessor(templates=[t]) def test_template_processor_with_custom_domain_catalog(): @@ -246,7 +256,9 @@ def test_template_processor_with_custom_domain_catalog(): layout_yaml = """ version: "0.1" -templateId: MedicalReport +name: MedicalReport +catalogs: + - "https://company.org/custom_medical_catalog.json" parameters: patientName: {type: string} layout: @@ -255,7 +267,7 @@ def test_template_processor_with_custom_domain_catalog(): name: ${patientName} """ tmpl = StaticTemplate.from_yaml(layout_yaml) - processor = TemplateProcessor(templates=[tmpl], base_catalog=custom_catalog) + processor = TemplateProcessor(templates=[tmpl], catalogs=custom_catalog) assert ( processor.base_catalog_id == "https://company.org/custom_medical_catalog.json" @@ -269,6 +281,97 @@ def test_template_processor_with_custom_domain_catalog(): assert "Button" not in cat["components"] +def test_multi_catalog_resolution_and_disambiguation(): + """Verifies multi-catalog resolution, collision detection, and explicit catalogId disambiguation.""" + catalog_a = { + "$id": "https://a2ui.org/cat_a.json", + "catalogId": "https://a2ui.org/cat_a.json", + "components": { + "SharedBox": {"type": "object"}, + "CompA": {"type": "object"}, + }, + } + catalog_b = { + "$id": "https://a2ui.org/cat_b.json", + "catalogId": "https://a2ui.org/cat_b.json", + "components": { + "SharedBox": {"type": "object"}, + "CompB": {"type": "object"}, + }, + } + + # Case 1: Ambiguous component without disambiguation raises ValueError + ambiguous_yaml = """ +version: "0.1" +name: AmbiguousTemplate +catalogs: + - "https://a2ui.org/cat_a.json" + - "https://a2ui.org/cat_b.json" +parameters: {} +layout: + component: SharedBox +""" + tmpl_ambiguous = StaticTemplate.from_yaml(ambiguous_yaml) + with pytest.raises(ValueError, match="ambiguous across multiple declared catalogs"): + TemplateProcessor( + templates=[tmpl_ambiguous], + catalogs=[catalog_a, catalog_b], + version="v1.0", + ) + + # Case 2: Disambiguated component with catalogId resolves successfully + disambiguated_yaml = """ +version: "0.1" +name: DisambiguatedTemplate +catalogs: + - "https://a2ui.org/cat_a.json" + - "https://a2ui.org/cat_b.json" +parameters: {} +layout: + component: SharedBox + catalogId: "https://a2ui.org/cat_b.json" +""" + tmpl_ok = StaticTemplate.from_yaml(disambiguated_yaml) + proc = TemplateProcessor( + templates=[tmpl_ok], + catalogs=[catalog_a, catalog_b], + version="v1.0", + ) + expanded = proc.expand_template("inst_1", "DisambiguatedTemplate", {}) + assert len(expanded) == 1 + assert expanded[0]["component"] == "SharedBox" + assert expanded[0]["catalogId"] == "https://a2ui.org/cat_b.json" + + +def test_v09_rejects_multi_catalog_template(): + """Verifies that A2UI v0.9 strictly rejects templates declaring multiple catalogs.""" + catalog_a = { + "$id": "https://a2ui.org/cat_a.json", + "components": {"CompA": {}}, + } + catalog_b = { + "$id": "https://a2ui.org/cat_b.json", + "components": {"CompB": {}}, + } + multi_yaml = """ +version: "0.1" +name: MultiCatTemplate +catalogs: + - "https://a2ui.org/cat_a.json" + - "https://a2ui.org/cat_b.json" +parameters: {} +layout: + component: CompA +""" + tmpl = StaticTemplate.from_yaml(multi_yaml) + with pytest.raises(ValueError, match="only supports a single catalog"): + TemplateProcessor( + templates=[tmpl], + catalogs=[catalog_a, catalog_b], + version="v0.9.1", + ) + + def test_type_aliases_and_private_helpers(): """Verifies that semantic type aliases and backwards-compatible private functions are available.""" assert TemplateId is str @@ -276,7 +379,6 @@ def test_type_aliases_and_private_helpers(): assert ComponentId is str assert ParamPath is str - # Test private helpers params: TemplateParams = {"user": {"profile": {"tier": "Pro"}}} found, val = _resolve_param_path("user.profile.tier", params) assert found is True @@ -286,11 +388,13 @@ def test_type_aliases_and_private_helpers(): assert sub == "Pro" t = StaticTemplate( + name="Dummy", template_id="Dummy", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], version="0.1", parameters={}, components=[], ) - p = TemplateProcessor(templates=[t]) + p = TemplateProcessor(templates=[t], catalogs=BASIC_CATALOG) p._validate_templates() - p.validate_templates() # test deprecated public alias + p.validate_templates() diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py index 75f1a04781..e489852806 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_template_conformance.py @@ -14,9 +14,7 @@ """Conformance test runner for A2UI Templates executing declarative YAML suites.""" -import glob import json -import os from pathlib import Path from typing import Any, Dict, List, Tuple import jsonschema @@ -32,6 +30,20 @@ ) +BASIC_CATALOG_PATH = ( + Path(__file__).resolve().parents[7] + / "specification" + / "v0_9_1" + / "catalogs" + / "basic" + / "catalog.json" +) +BASIC_CATALOG: Dict[str, Any] = {} +if BASIC_CATALOG_PATH.is_file(): + with open(BASIC_CATALOG_PATH, "r", encoding="utf-8") as f: + BASIC_CATALOG = json.load(f) + + def get_conformance_dir() -> Path: """Dynamically resolves the conformance directory regardless of nesting.""" curr = Path(__file__).resolve().parent @@ -84,7 +96,10 @@ def _execute_case(case: Dict[str, Any]) -> Any: templates.append(StaticTemplate.from_dict(t_dict)) base_catalog = case.get("base_catalog", None) - processor = TemplateProcessor(templates=templates, base_catalog=base_catalog) + catalogs = case.get("catalogs", None) or base_catalog + if catalogs is None and BASIC_CATALOG: + catalogs = [BASIC_CATALOG] + processor = TemplateProcessor(templates=templates, catalogs=catalogs) template_id = case.get( "template_id", @@ -108,7 +123,10 @@ def _execute_case(case: Dict[str, Any]) -> Any: templates.append(StaticTemplate.from_dict(t_dict)) base_catalog = case.get("base_catalog", None) - processor = TemplateProcessor(templates=templates, base_catalog=base_catalog) + catalogs = case.get("catalogs", None) or base_catalog + if catalogs is None and BASIC_CATALOG: + catalogs = [BASIC_CATALOG] + processor = TemplateProcessor(templates=templates, catalogs=catalogs) return processor.process_message(case["message"]) From 6eb9ff18a0eac6c5c7471084cbd3d36183197053 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:34:13 +0930 Subject: [PATCH 13/26] feat(python-sdk): add @dynamic_template decorator and __call__ invocation --- .../experimental/template/__init__.py | 2 + .../experimental/template/models.py | 57 +++++++++++++++++++ .../experimental/template/test_models.py | 56 ++++++++++++++++++ .../experimental/template/test_processor.py | 31 ++++++++++ 4 files changed, 146 insertions(+) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py index fb917bb24f..976321b584 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/__init__.py @@ -18,6 +18,7 @@ Template, StaticTemplate, DynamicTemplate, + dynamic_template, BaseTemplate, Param, ParamType, @@ -50,6 +51,7 @@ "Template", "StaticTemplate", "DynamicTemplate", + "dynamic_template", "BaseTemplate", "Param", "ParamType", diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py index 99311c0ed7..9afda3d76b 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -18,6 +18,7 @@ import asyncio from enum import Enum +import functools import inspect import json import os @@ -945,8 +946,64 @@ def to_dict(self) -> Dict[str, Any]: res["sampleData"] = self.sample_data return res + def __call__(self, *args: Any, **kwargs: Any) -> Any: + """Allows the DynamicTemplate instance to remain directly callable in unit tests or user code.""" + if self.render_fn is not None: + return self.render_fn(*args, **kwargs) + if self.resolver is not None: + return self.resolve(kwargs if kwargs else (args[0] if args else {})) + raise TypeError(f"DynamicTemplate '{self.name}' is not callable.") + def to_yaml(self) -> str: """Serializes the dynamic template or underlying layout to a clean YAML string.""" if self.layout is not None: return self.layout.to_yaml() return yaml.dump(self.to_dict(), sort_keys=False) + + +def dynamic_template( + name_or_fn: Optional[Union[str, Callable[..., Any]]] = None, + *, + name: Optional[str] = None, + catalogs: Optional[Union[List[str], str]] = None, + id: Optional[str] = None, + imports: Optional[Union[List[str], Dict[str, str]]] = None, + description: Optional[str] = None, + sample_data: Optional[Dict[str, Any]] = None, + version: str = "0.1", + **kwargs: Any, +) -> Union[DynamicTemplate, Callable[[Callable[..., Any]], DynamicTemplate]]: + """Decorator to declare an A2UI DynamicTemplate directly on a Python render function. + + Can be used with or without arguments: + @dynamic_template(catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"]) + def user_card(title: str): ... + + @dynamic_template + def user_card(title: str): ... + """ + + def _decorator(fn: Callable[..., Any]) -> DynamicTemplate: + explicit_name = name or (name_or_fn if isinstance(name_or_fn, str) else None) + tmpl_name = explicit_name or "".join( + word.capitalize() for word in fn.__name__.split("_") + ) + desc = description or (fn.__doc__.strip() if fn.__doc__ else None) + + tmpl = DynamicTemplate( + name=tmpl_name, + catalogs=catalogs, + id=id, + imports=imports, + render=fn, + description=desc, + sample_data=sample_data, + version=version, + **kwargs, + ) + functools.update_wrapper(tmpl, fn) + return tmpl + + if callable(name_or_fn): + return _decorator(name_or_fn) + return _decorator diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py index ba6b425a57..cba1466cb7 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_models.py @@ -18,6 +18,7 @@ from a2ui.inference_formats.experimental.template.models import ( DynamicTemplate, + dynamic_template, ParamType, normalize_node, ) @@ -71,3 +72,58 @@ def my_resolver(userId: str, count: int = 5, active: bool = True) -> dict: assert "active" in params assert params["active"].type == ParamType.BOOLEAN assert params["active"].default is True + + +def test_dynamic_template_decorator_and_callability(): + """Verifies that @dynamic_template decorates a function, infers metadata, and preserves callability.""" + + @dynamic_template( + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + description="Generates a user badge with custom status.", + ) + def user_badge(user_name: str, status: str = "Active", score: int = 100): + """Generates a user badge.""" + return { + "component": "Card", + "child": { + "component": "Text", + "text": f"{user_name}: {status} ({score})", + }, + } + + # 1. Verify it acts as a DynamicTemplate instance + assert isinstance(user_badge, DynamicTemplate) + assert user_badge.name == "UserBadge" + assert user_badge.description == "Generates a user badge with custom status." + assert user_badge.catalogs == [ + "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" + ] + assert "user_name" in user_badge.parameters + assert user_badge.parameters["user_name"].type == ParamType.STRING + assert user_badge.parameters["status"].default == "Active" + assert user_badge.parameters["score"].type == ParamType.INTEGER + assert user_badge.parameters["score"].default == 100 + + # 2. Verify it remains directly callable as a standard Python function + rendered = user_badge("Alice", status="Online", score=150) + assert isinstance(rendered, dict) + assert rendered["component"] == "Card" + assert rendered["child"]["text"] == "Alice: Online (150)" + + +def test_dynamic_template_decorator_no_args(): + """Verifies @dynamic_template works without parenthesis, extracting docstring and PascalCase name.""" + + @dynamic_template + def metric_tile(label: str, value: float = 0.0): + """KPI metric display tile.""" + return {"component": "Text", "text": f"{label}: {value}"} + + assert isinstance(metric_tile, DynamicTemplate) + assert metric_tile.name == "MetricTile" + assert metric_tile.description == "KPI metric display tile." + assert "label" in metric_tile.parameters + assert metric_tile.parameters["value"].type == ParamType.NUMBER + + res = metric_tile("CPU", 99.4) + assert res == {"component": "Text", "text": "CPU: 99.4"} diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py index 812ccfb3fb..5a15b30fb2 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py @@ -22,6 +22,7 @@ from a2ui.inference_formats.experimental.template import ( DynamicTemplate, + dynamic_template, StaticTemplate, TemplateProcessor, TemplateId, @@ -398,3 +399,33 @@ def test_type_aliases_and_private_helpers(): p = TemplateProcessor(templates=[t], catalogs=BASIC_CATALOG) p._validate_templates() p.validate_templates() + + +def test_dynamic_template_decorator_processor_expansion(): + """Verifies that @dynamic_template can be directly registered and expanded by TemplateProcessor.""" + + @dynamic_template( + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + description="Renders a KPI metric display.", + ) + def kpi_metric(label: str, value: str, change: str = "+0.0%"): + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + {"component": "Text", "text": f"{label}: {value}"}, + {"component": "Text", "text": f"Change: {change}"}, + ], + }, + } + + processor = TemplateProcessor(templates=[kpi_metric], catalogs=BASIC_CATALOG) + expanded = processor.expand_template( + "inst_kpi", + "KpiMetric", + {"label": "Revenue", "value": "$1.2M", "change": "+12%"}, + ) + assert len(expanded) == 4 + assert any(c.get("text") == "Revenue: $1.2M" for c in expanded) + assert any(c.get("text") == "Change: +12%" for c in expanded) From ee2885e35025b49feefec6dde61b7719ed15c9db Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:22:39 +0930 Subject: [PATCH 14/26] feat(python-sdk): standardize MAX_EXPANSION_DEPTH default to 50 --- .../inference_formats/experimental/template/processor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py index b3aaea83c2..7f2e732ad3 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/processor.py @@ -70,7 +70,11 @@ CatalogSchema: TypeAlias = Dict[str, Any] JSONSchemaDict: TypeAlias = Dict[str, Any] +# 5. Safety & Recursion Limits +MAX_EXPANSION_DEPTH: int = 50 + __all__ = [ + "MAX_EXPANSION_DEPTH", "TemplateProcessor", "TemplateId", "InstanceId", @@ -435,7 +439,7 @@ def expand_template( f" {template_id}" ) - if _depth > 32: + if _depth > MAX_EXPANSION_DEPTH: raise RecursionError( f"Maximum template expansion depth exceeded ({_depth}) at" f" '{template_id}'. Call stack: {' -> '.join(_call_stack)}" From 9ba72fc1b3b78ebfec2709be784bbf37df2bf630 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:29:41 +0930 Subject: [PATCH 15/26] feat(python-sdk): support double-curly Mustache syntax for template substitutions --- .../experimental/template/models.py | 2 +- .../experimental/template/processor.py | 32 ++++++---- .../conformance/suites/01_substitution.yaml | 53 +++++++++++++++ .../experimental/template/test_processor.py | 64 +++++++++++++++++++ 4 files changed, 139 insertions(+), 12 deletions(-) diff --git a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py index 9afda3d76b..36268835ee 100644 --- a/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py +++ b/agent_sdks/python/a2ui_agent/src/a2ui/inference_formats/experimental/template/models.py @@ -681,7 +681,7 @@ def _collect_loop_vars(val: Any, loop_vars: set) -> None: StaticTemplate._collect_loop_vars(item, loop_vars) def _check_val(self, val: Any, loop_vars: set) -> None: - expr_pattern = re.compile(r"\$\{([\w\.]+)\}") + expr_pattern = re.compile(r"(? Any: return [_substitute_params(x, params) for x in val] elif isinstance(val, str): - # 1. Exact parameter match preserving type: e.g. "${user}" or "${score}" - match = re.match(r"^\$\{([\w\.]+)\}$", val) + # 1. Exact parameter match preserving type: e.g. "{{ user }}", "{{user}}", "${user}" + match = re.match(r"^(?:\{\{\s*|\$\{)([\w\.]+)(?:\s*\}\}|\})$", val) if match: param_path = match.group(1) found, res = _resolve_param_path(param_path, params) if found: return res - # 2. Check for exact escaped token match: e.g. r"\${user}" -> "${user}" - escaped_match = re.match(r"^\\(\$\{[\w\.]+\})$", val) + # 2. Check for exact escaped token match: e.g. r"\{{ user }}" -> "{{ user }}" or r"\${user}" -> "${user}" + escaped_match = re.match(r"^\\((?:\{\{|\$\{)[^}]*(?:\}\}|\}))$", val) if escaped_match: return escaped_match.group(1) # 3. In-string replacement with escape handling: - # Negative lookbehind (? str: path = m.group(1) found, res = _resolve_param_path(path, params) @@ -176,10 +175,13 @@ def replacer(m: re.Match[str]) -> str: return str(res) return str(m.group(0)) - # Replace unescaped tokens - substituted = re.sub(r"(? "${path}" - final_str = re.sub(r"\\(\$\{[\w\.]+\})", r"\1", substituted) + # Replace unescaped Mustache tokens: (? "{{...}}" and r"\${...}" -> "${...}" + final_str = re.sub(r"\\(\{\{.*?\}\}|\$\{.*?\})", r"\1", substituted) return final_str return val @@ -587,7 +589,14 @@ def _resolve_slot(val: Any, params: TemplateParams) -> Tuple[bool, Any]: if "default" in val: return True, val["default"] elif isinstance(val, str): - if val.startswith("${") and val.endswith("}"): + s = val.strip() + if s.startswith("{{") and s.endswith("}}"): + p_path = s[2:-2].strip() + found, res = _resolve_param_path(p_path, params) + if found: + return True, res + return True, None + elif val.startswith("${") and val.endswith("}"): p_path = val[2:-1] found, res = _resolve_param_path(p_path, params) if found: @@ -680,7 +689,8 @@ def _expand_static_layout( and not ( isinstance(params[p_name], str) and ( - params[p_name].startswith("${") + params[p_name].strip().startswith("{{") + or params[p_name].startswith("${") or params[p_name].startswith("__") ) ) diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml index e0f4269741..dc8bbfb471 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/conformance/suites/01_substitution.yaml @@ -163,3 +163,56 @@ default: Contributor params: {} expect: Contributor +- name: test_mustache_exact_integer_preservation + description: Verifies that exact Mustache token match preserves integer type. + action: substitute_params + args: + value: "{{ count }}" + params: + count: 42 + expect: 42 +- name: test_mustache_exact_boolean_preservation + description: Verifies that exact Mustache token match preserves boolean type. + action: substitute_params + args: + value: "{{ isEnabled }}" + params: + isEnabled: true + expect: true +- name: test_mustache_exact_array_preservation + description: Verifies that exact Mustache token match preserves array data structure. + action: substitute_params + args: + value: "{{ tags }}" + params: + tags: + - admin + - developer + expect: + - admin + - developer +- name: test_mustache_string_interpolation + description: Verifies multiple Mustache tokens interpolated inside string. + action: substitute_params + args: + value: "{{ dept }} - Grade {{ grade }}" + params: + dept: Eng + grade: 9 + expect: "Eng - Grade 9" +- name: test_mustache_escaping + description: Verifies that escaped Mustache syntax is emitted as a literal. + action: substitute_params + args: + value: 'Literal \{{ keep }} and dynamic {{ replace }}' + params: + replace: substituted + expect: "Literal {{ keep }} and dynamic substituted" +- name: test_mustache_client_data_binding_passthrough + description: Verifies that client ${/data/path} bindings are preserved untouched. + action: substitute_params + args: + value: "Client: ${/user/name}, Server: {{ serverVal }}" + params: + serverVal: local + expect: "Client: ${/user/name}, Server: local" diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py index 5a15b30fb2..0c1d0504f9 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py @@ -429,3 +429,67 @@ def kpi_metric(label: str, value: str, change: str = "+0.0%"): assert len(expanded) == 4 assert any(c.get("text") == "Revenue: $1.2M" for c in expanded) assert any(c.get("text") == "Change: +12%" for c in expanded) + + +def test_mustache_substitution_syntax(): + """Verifies that double-curly Mustache syntax {{ param }} is supported for substitutions, + preserves exact types, allows whitespace, and leaves client ${/data/path} bindings intact.""" + layout_yaml = """ +version: "0.1" +name: MustacheCard +catalogs: + - "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" +parameters: + count: {type: integer} + isActive: {type: boolean} + user: {type: object} + tags: {type: array} + dept: {type: string} +layout: + component: Card + child: + component: Column + children: + - component: Text + text: "{{ count }}" + - component: Text + text: "{{ isActive }}" + - component: Text + text: "Hello, {{ user.profile.name }}! Dept: {{ dept }}" + - component: Text + text: "Literal: \\\\{{ escaped }}" + - component: Text + text: + call: formatString + args: + value: "Live: ${/metrics/cpu}, Dept: {{ dept }}" +""" + tmpl = StaticTemplate.from_yaml(layout_yaml) + processor = TemplateProcessor(templates=[tmpl], catalogs=BASIC_CATALOG) + expanded = processor.expand_template( + "inst_1", + "MustacheCard", + { + "count": 42, + "isActive": True, + "user": {"profile": {"name": "Alice"}}, + "tags": ["a", "b"], + "dept": "Engineering", + }, + ) + + texts = [c for c in expanded if c.get("component") == "Text"] + # 1. Exact match integer type preserved + assert texts[0]["text"] == 42 + # 2. Exact match boolean with whitespace preserved + assert texts[1]["text"] is True + # 3. Embedded interpolation with dot-notation + assert texts[2]["text"] == "Hello, Alice! Dept: Engineering" + # 4. Escaped literal unescaped + assert texts[3]["text"] == "Literal: {{ escaped }}" + # 5. Client data-model path ${/metrics/cpu} preserved untouched while {{ dept }} substituted + assert texts[4]["text"] == { + "call": "formatString", + "args": {"value": "Live: ${/metrics/cpu}, Dept: Engineering"}, + } + From 3ec3f8597be324d064942544413cf4ff6d140c97 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:34:18 +0930 Subject: [PATCH 16/26] style(python-sdk): format test_processor with pyink --- .../inference_formats/experimental/template/test_processor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py index 0c1d0504f9..0b5668a1c5 100644 --- a/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py +++ b/agent_sdks/python/a2ui_agent/tests/inference_formats/experimental/template/test_processor.py @@ -433,7 +433,8 @@ def kpi_metric(label: str, value: str, change: str = "+0.0%"): def test_mustache_substitution_syntax(): """Verifies that double-curly Mustache syntax {{ param }} is supported for substitutions, - preserves exact types, allows whitespace, and leaves client ${/data/path} bindings intact.""" + preserves exact types, allows whitespace, and leaves client ${/data/path} bindings intact. + """ layout_yaml = """ version: "0.1" name: MustacheCard @@ -492,4 +493,3 @@ def test_mustache_substitution_syntax(): "call": "formatString", "args": {"value": "Live: ${/metrics/cpu}, Dept: Engineering"}, } - From 6a1e8326033d89d4850e1a27be6452f563ff62fe Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 13:12:39 +0930 Subject: [PATCH 17/26] feat(samples): add community templates sample app with version 0.1 templates and interactive studio --- samples/community/package.json | 3 +- samples/community/templates/README.md | 50 + .../templates/client/.prettierignore | 2 + .../templates/client/eslint.config.mjs | 37 + samples/community/templates/client/index.html | 60 + .../community/templates/client/package.json | 51 + .../community/templates/client/src/App.tsx | 2027 +++++++++++++++ .../community/templates/client/src/main.tsx | 25 + .../community/templates/client/tsconfig.json | 20 + .../community/templates/client/vite.config.ts | 26 + samples/community/templates/pyproject.toml | 30 + samples/community/templates/server.py | 642 +++++ .../templates/templates/feedback_item.yaml | 46 + .../templates/templates/goal_item.yaml | 59 + .../templates/templates/salary_card.yaml | 116 + .../templates/templates/section_card.yaml | 52 + .../templates/templates/team_card.yaml | 68 + .../templates/team_feedback_board.yaml | 64 + .../templates/templates/team_goal_list.yaml | 65 + .../team_member_knowledge_panel.yaml | 78 + .../templates/templates/team_roster.yaml | 29 + .../templates/two_column_layout.yaml | 40 + .../templates/templates/user_profile.yaml | 38 + samples/community/templates/test_e2e.mjs | 256 ++ samples/community/templates/uv.lock | 2231 +++++++++++++++++ samples/community/yarn.lock | 1525 ++++++++++- 26 files changed, 7621 insertions(+), 19 deletions(-) create mode 100644 samples/community/templates/README.md create mode 100644 samples/community/templates/client/.prettierignore create mode 100644 samples/community/templates/client/eslint.config.mjs create mode 100644 samples/community/templates/client/index.html create mode 100644 samples/community/templates/client/package.json create mode 100644 samples/community/templates/client/src/App.tsx create mode 100644 samples/community/templates/client/src/main.tsx create mode 100644 samples/community/templates/client/tsconfig.json create mode 100644 samples/community/templates/client/vite.config.ts create mode 100644 samples/community/templates/pyproject.toml create mode 100644 samples/community/templates/server.py create mode 100644 samples/community/templates/templates/feedback_item.yaml create mode 100644 samples/community/templates/templates/goal_item.yaml create mode 100644 samples/community/templates/templates/salary_card.yaml create mode 100644 samples/community/templates/templates/section_card.yaml create mode 100644 samples/community/templates/templates/team_card.yaml create mode 100644 samples/community/templates/templates/team_feedback_board.yaml create mode 100644 samples/community/templates/templates/team_goal_list.yaml create mode 100644 samples/community/templates/templates/team_member_knowledge_panel.yaml create mode 100644 samples/community/templates/templates/team_roster.yaml create mode 100644 samples/community/templates/templates/two_column_layout.yaml create mode 100644 samples/community/templates/templates/user_profile.yaml create mode 100644 samples/community/templates/test_e2e.mjs create mode 100644 samples/community/templates/uv.lock diff --git a/samples/community/package.json b/samples/community/package.json index 8664559263..f71174a455 100644 --- a/samples/community/package.json +++ b/samples/community/package.json @@ -11,7 +11,8 @@ "mcp/*", "mcp/*/client", "mcp/*/apps/src", - "mcp/a2ui-in-mcpapps/server/apps/*" + "mcp/a2ui-in-mcpapps/server/apps/*", + "templates/client" ], "scripts": { "build:web": "yarn workspace angular-a2ui run build && yarn workspace @a2ui/mcp-apps-in-a2ui-sample run build && yarn workspace personalized-learning-demo run build && yarn workspace a2ui-in-mcpapps-client run build && yarn workspace a2ui-in-mcpapps-client run build:sandbox && yarn workspace basic-mcp-app-angular run build:all && yarn workspace basic-mcp-app-angular-editor run build:all && yarn workspace a2ui-over-mcp-recipe-client run build && yarn workspace mcp-calculator-app run build" diff --git a/samples/community/templates/README.md b/samples/community/templates/README.md new file mode 100644 index 0000000000..a05f8146b0 --- /dev/null +++ b/samples/community/templates/README.md @@ -0,0 +1,50 @@ +# A2UI templates community demo + +This sample demonstrates server-side template expansion in the A2UI Python Agent SDK using the standard Basic Catalog and React client renderer. + +--- + +## Overview + +- **Static declarative templates**: Parameterized layout subtrees (such as `UserProfile`, `TeamCard`, `TeamRoster`, `TeamGoalList`, `TeamFeedbackBoard`) defined in JSON and expanded into Basic Catalog primitives (`Card`, `Column`, `Row`, `Text`, `Divider`, `Icon`, `Button`). +- **Dynamic server resolvers**: Programmatic templates (such as `EmployeeSalaryCard`) that run Python resolver callbacks to query internal databases. The model only receives and passes identifiers, while sensitive numbers are injected server-side. +- **Express DSL & synchronous expansion**: The language model outputs concise Express DSL. The backend parser expands templates synchronously without requiring custom client components. +- **Interactive studio & library**: The React client includes an Interactive Chat with suggested prompt chips and latency/token metrics, alongside a 3-Stage Dynamic Template Studio showing input arguments, underlying blueprint JSON, and live rendered output. + +--- + +## Running the demo + +### 1. Start the backend server + +```bash +cd samples/community/templates +uv run uvicorn server:app --reload --port 8000 +``` + +To test live Gemini generation in addition to the preset templates, set your API key and optional model: + +```bash +export GEMINI_API_KEY="your-api-key" +export GEMINI_MODEL="gemini-flash-latest" # Optional, defaults to gemini-flash-latest +``` + +### 2. Start the frontend client + +```bash +cd samples/community/templates/client +yarn install +yarn dev +``` + +Open [http://localhost:5173](http://localhost:5173) in your browser. + +--- + +## Testing + +Run the Playwright end-to-end integration test suite: + +```bash +node samples/community/templates/test_e2e.mjs +``` diff --git a/samples/community/templates/client/.prettierignore b/samples/community/templates/client/.prettierignore new file mode 100644 index 0000000000..5e71162a6b --- /dev/null +++ b/samples/community/templates/client/.prettierignore @@ -0,0 +1,2 @@ +dist/ +.wireit/ diff --git a/samples/community/templates/client/eslint.config.mjs b/samples/community/templates/client/eslint.config.mjs new file mode 100644 index 0000000000..ba401290a5 --- /dev/null +++ b/samples/community/templates/client/eslint.config.mjs @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import tseslint from 'typescript-eslint'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; + +export default tseslint.config( + { + ignores: ['dist/**', 'vite.config.*', '**/node_modules/**', '**/.yarn/**'], + }, + { + files: ['src/**/*.{ts,tsx}'], + extends: [tseslint.configs.recommended], + plugins: { + react: reactPlugin, + 'react-hooks': reactHooksPlugin, + }, + rules: { + 'react/react-in-jsx-scope': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, + }, +); diff --git a/samples/community/templates/client/index.html b/samples/community/templates/client/index.html new file mode 100644 index 0000000000..3570cc797f --- /dev/null +++ b/samples/community/templates/client/index.html @@ -0,0 +1,60 @@ + + + + + + + + A2UI Templates - Community Demo + + + + + + + + +
+ + + diff --git a/samples/community/templates/client/package.json b/samples/community/templates/client/package.json new file mode 100644 index 0000000000..691df47443 --- /dev/null +++ b/samples/community/templates/client/package.json @@ -0,0 +1,51 @@ +{ + "name": "@a2ui/templates-community-demo", + "private": true, + "version": "0.1.0", + "description": "A2UI Templates Community Demo Client", + "type": "module", + "scripts": { + "dev": "vite", + "build": "wireit", + "preview": "vite preview", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "node -e 'console.log(\"Workspace has no tests.\");'" + }, + "dependencies": { + "@a2ui/react": "^0.10.2", + "@a2ui/web_core": "^0.10.6", + "react": "^19.2.7", + "react-dom": "^19.2.7" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "eslint": "^10.4.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.1.1", + "typescript": "5.9.3", + "typescript-eslint": "^8.61.0", + "vite": "^8.0.16", + "wireit": "^0.15.0-pre.2" + }, + "wireit": { + "build": { + "command": "tsc -b && vite build", + "files": [ + "src/**/*", + "index.html", + "tsconfig.json", + "vite.config.*" + ], + "output": [ + "dist/" + ] + } + } +} diff --git a/samples/community/templates/client/src/App.tsx b/samples/community/templates/client/src/App.tsx new file mode 100644 index 0000000000..e82f584c98 --- /dev/null +++ b/samples/community/templates/client/src/App.tsx @@ -0,0 +1,2027 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useState, useEffect, useRef} from 'react'; +import {MessageProcessor} from '@a2ui/web_core/v0_9'; +import {A2uiSurface, basicCatalog} from '@a2ui/react/v0_9'; + +interface FeedItem { + id: string; + type: 'user' | 'assistant'; + text?: string; + surfaceId?: string; + raw?: string; + messages?: any[]; + metrics?: { + latency?: number; + thinkingTokens?: number; + outputTokens?: number; + promptTokens?: number; + totalTokens?: number; + isPreset?: boolean; + }; +} + +interface TemplateDefinition { + version?: string; + templateId: string; + parameters: Record; + components?: any[]; + layout?: Record; + yamlContent?: string; + description?: string; + sampleData?: Record; + sampleMessages?: any[]; + isDynamic?: boolean; + isProgrammatic?: boolean; + isDataBinding?: boolean; + renderSource?: string; + layoutTemplate?: Record; + layoutTemplateYaml?: string; + resolvedData?: Record; + availablePresets?: Array<{label: string; value: string}>; +} + +const A2UI_THEME_VARS: React.CSSProperties = { + // Card & Container + ['--a2ui-card-border-radius' as any]: '16px', + ['--a2ui-card-background' as any]: '#ffffff', + ['--a2ui-card-border' as any]: '1px solid #e2e8f0', + ['--a2ui-card-box-shadow' as any]: + '0 4px 12px -2px rgba(15, 23, 42, 0.06), 0 2px 6px -1px rgba(15, 23, 42, 0.03)', + ['--a2ui-card-padding' as any]: '18px 22px', + ['--a2ui-card-margin' as any]: '8px 0', + + // Primitives & General + ['--a2ui-border-radius' as any]: '12px', + ['--a2ui-color-border' as any]: '#e2e8f0', + ['--a2ui-color-surface' as any]: '#ffffff', + ['--a2ui-color-on-surface' as any]: '#0f172a', + + // Primary Action & Button + ['--a2ui-color-primary' as any]: '#2563eb', + ['--a2ui-color-primary-hover' as any]: '#1d4ed8', + ['--a2ui-color-on-primary' as any]: '#ffffff', + ['--a2ui-button-border-radius' as any]: '10px', + ['--a2ui-button-background' as any]: '#2563eb', + ['--a2ui-button-padding' as any]: '8px 18px', + ['--a2ui-button-font-weight' as any]: '600', + ['--a2ui-button-box-shadow' as any]: '0 1px 2px rgba(37, 99, 235, 0.2)', + + // Spacing & Icons + ['--a2ui-spacing-s' as any]: '6px', + ['--a2ui-spacing-m' as any]: '12px', + ['--a2ui-spacing-l' as any]: '20px', + ['--a2ui-icon-size' as any]: '22px', + ['--a2ui-icon-color' as any]: '#2563eb', + + // Typography & Dividers + ['--a2ui-divider-color' as any]: '#f1f5f9', + ['--a2ui-text-caption-color' as any]: '#64748b', +}; + +export default function App() { + const [currentView, setCurrentView] = useState<'chat' | 'library'>('chat'); + + // Chat State + const [chatProcessor] = useState(() => new MessageProcessor([basicCatalog])); + const [, setChatTick] = useState(0); + const [feed, setFeed] = useState([]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const [activeInspector, setActiveInspector] = useState(null); + const [inspectorTab, setInspectorTab] = useState<'express' | 'json'>('express'); + const [copiedTurn, setCopiedTurn] = useState(false); + const chatBottomRef = useRef(null); + + // Library State + const [libraryProcessor] = useState(() => new MessageProcessor([basicCatalog])); + const [, setLibraryTick] = useState(0); + const [templates, setTemplates] = useState([]); + const [selectedTemplateId, setSelectedTemplateId] = useState('UserProfile'); + const [libraryLoading, setLibraryLoading] = useState(false); + const [copiedTemplate, setCopiedTemplate] = useState(false); + + // Dynamic Template Interactive State + const [selectedDynamicEmpId, setSelectedDynamicEmpId] = useState('emp_101'); + const [payrollDept, setPayrollDept] = useState('Global Engineering'); + const [payrollIncludeBonus, setPayrollIncludeBonus] = useState(true); + const [dynamicResolvedData, setDynamicResolvedData] = useState | null>(null); + const [dynamicTab, setDynamicTab] = useState<'input' | 'layout' | 'resolved'>('input'); + const [dynamicResolving, setDynamicResolving] = useState(false); + + // Subscriptions + useEffect(() => { + const forceUpdate = () => setChatTick(t => t + 1); + const subCreated = chatProcessor.onSurfaceCreated(forceUpdate); + const subDeleted = chatProcessor.onSurfaceDeleted(forceUpdate); + return () => { + subCreated.unsubscribe(); + subDeleted.unsubscribe(); + }; + }, [chatProcessor]); + + useEffect(() => { + const forceUpdate = () => setLibraryTick(t => t + 1); + const subCreated = libraryProcessor.onSurfaceCreated(forceUpdate); + const subDeleted = libraryProcessor.onSurfaceDeleted(forceUpdate); + return () => { + subCreated.unsubscribe(); + subDeleted.unsubscribe(); + }; + }, [libraryProcessor]); + + useEffect(() => { + if (currentView === 'chat') { + chatBottomRef.current?.scrollIntoView({behavior: 'smooth'}); + } + }, [feed, loading, currentView]); + + useEffect(() => { + const fetchTemplates = async () => { + setLibraryLoading(true); + try { + const res = await fetch('http://127.0.0.1:8000/templates'); + if (res.ok) { + const list: TemplateDefinition[] = await res.json(); + setTemplates(list); + if (list.length > 0) { + setSelectedTemplateId(prev => + list.find(t => t.templateId === prev) ? prev : list[0].templateId, + ); + for (const item of list) { + if (item.sampleMessages && item.sampleMessages.length > 0) { + libraryProcessor.processMessages(item.sampleMessages); + if (item.isDynamic) { + const empId = item.sampleData?.employeeId || 'emp_101'; + const dynamicSurfaceId = `preview_${item.templateId}_${empId}`; + const dynamicMsgs = item.sampleMessages.map((m: any) => { + if (m.createSurface) { + return { + ...m, + createSurface: { + ...m.createSurface, + surfaceId: dynamicSurfaceId, + }, + }; + } + if (m.updateComponents) { + return { + ...m, + updateComponents: { + ...m.updateComponents, + surfaceId: dynamicSurfaceId, + }, + }; + } + return m; + }); + libraryProcessor.processMessages(dynamicMsgs); + } + } + } + } + } + } catch (e) { + console.error('Failed to load templates list:', e); + } finally { + setLibraryLoading(false); + } + }; + if (currentView === 'library' || templates.length === 0) { + fetchTemplates(); + } + }, [libraryProcessor, currentView]); + + const selectedTemplate = templates.find(t => t.templateId === selectedTemplateId); + + // When dynamic template selection changes, sync initial state + useEffect(() => { + if (selectedTemplate?.isDynamic) { + if (selectedTemplate.resolvedData) { + setDynamicResolvedData(selectedTemplate.resolvedData); + } + if (selectedTemplate.sampleData?.employeeId) { + setSelectedDynamicEmpId(selectedTemplate.sampleData.employeeId); + } + if (selectedTemplate.sampleData?.department) { + setPayrollDept(selectedTemplate.sampleData.department); + } + if (selectedTemplate.sampleData?.includeBonus !== undefined) { + setPayrollIncludeBonus(selectedTemplate.sampleData.includeBonus); + } + } + }, [selectedTemplate]); + + const handleResolveDynamicTemplate = async (paramInput?: any) => { + if (!selectedTemplate) return; + setDynamicResolving(true); + let sendParams: Record = {}; + let surfaceSuffix = 'default'; + + if (selectedTemplate.templateId === 'PayrollSummary') { + const dept = + typeof paramInput === 'object' && paramInput.department !== undefined + ? paramInput.department + : payrollDept; + const bonus = + typeof paramInput === 'object' && paramInput.includeBonus !== undefined + ? paramInput.includeBonus + : payrollIncludeBonus; + sendParams = {department: dept, includeBonus: bonus}; + surfaceSuffix = `${dept.replace(/\s+/g, '_')}_${bonus}`; + } else { + const empId = typeof paramInput === 'string' ? paramInput : selectedDynamicEmpId; + setSelectedDynamicEmpId(empId); + sendParams = {employeeId: empId}; + surfaceSuffix = empId; + } + + try { + const res = await fetch( + `http://127.0.0.1:8000/templates/${selectedTemplate.templateId}/resolve`, + { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({params: sendParams}), + }, + ); + if (!res.ok) { + throw new Error(`Server returned status ${res.status}`); + } + const data = await res.json(); + setDynamicResolvedData(data.resolvedData); + if (data.sampleMessages) { + const dynamicSurfaceId = `preview_${selectedTemplate.templateId}_${surfaceSuffix}`; + const updatedMessages = data.sampleMessages.map((m: any) => { + if (m.createSurface) { + return { + ...m, + createSurface: { + ...m.createSurface, + surfaceId: dynamicSurfaceId, + }, + }; + } + if (m.updateComponents) { + return { + ...m, + updateComponents: { + ...m.updateComponents, + surfaceId: dynamicSurfaceId, + }, + }; + } + return m; + }); + libraryProcessor.processMessages(updatedMessages); + setLibraryTick(t => t + 1); + } + } catch (err) { + console.error('Failed to resolve dynamic template:', err); + } finally { + setDynamicResolving(false); + } + }; + + const copyToClipboard = (text: string, isTemplate = false) => { + navigator.clipboard.writeText(text); + if (isTemplate) { + setCopiedTemplate(true); + setTimeout(() => setCopiedTemplate(false), 2000); + } else { + setCopiedTurn(true); + setTimeout(() => setCopiedTurn(false), 2000); + } + }; + + const sendPrompt = async (promptText: string) => { + const text = promptText.trim(); + if (!text || loading) return; + + setInput(''); + const surfaceId = `surface_${Date.now()}`; + setFeed(prev => [...prev, {id: `user_${Date.now()}`, type: 'user', text}]); + setLoading(true); + + try { + const res = await fetch('http://127.0.0.1:8000/interact', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + prompt: text, + surfaceId, + }), + }); + + if (!res.ok) { + throw new Error(`Server returned status ${res.status}`); + } + + const data = await res.json(); + if (data.messages && data.messages.length > 0) { + chatProcessor.processMessages(data.messages); + } + + let actualSurfaceId = data.surfaceId || surfaceId; + if (data.messages && data.messages.length > 0) { + const found = + data.messages.find((m: any) => m.createSurface?.surfaceId)?.createSurface?.surfaceId || + data.messages.find((m: any) => m.updateComponents?.surfaceId)?.updateComponents + ?.surfaceId; + if (found) { + actualSurfaceId = found; + } + } + + setFeed(prev => [ + ...prev, + { + id: `assistant_${Date.now()}`, + type: 'assistant', + text: data.text, + surfaceId: actualSurfaceId, + raw: data.raw, + messages: data.messages, + metrics: data.metrics, + }, + ]); + } catch (err: any) { + setFeed(prev => [ + ...prev, + { + id: `assistant_${Date.now()}`, + type: 'assistant', + text: `Error contacting server: ${err.message}. Make sure the FastAPI server is running on http://127.0.0.1:8000.`, + surfaceId, + }, + ]); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Top Header */} +
+
+
+ + dashboard_customize + +
+
+

+ A2UI Templates +

+

+ Static & Dynamic Server Expansion · Basic Catalog +

+
+
+ + {/* View Switcher Tabs */} +
+ + +
+
+ + {/* Main Content View */} + {currentView === 'chat' ? ( +
+ {/* Presets Sidebar */} +
+
+

+ Example Presets +

+
+ {[ + { + label: '🔒 Verified Salary', + prompt: 'show verified salary', + desc: 'Dynamic server-resolved compensation', + }, + { + label: '💰 Payroll Summary', + prompt: 'show payroll summary', + desc: 'Programmatic dynamic template table', + }, + { + label: '📊 User Evaluation', + prompt: 'show user evaluation', + desc: 'Composite review & goals dashboard', + }, + { + label: '👤 User Profile', + prompt: 'show user profile', + desc: 'Single card profile', + }, + { + label: '👥 Team Roster', + prompt: 'show team roster', + desc: 'Nested team member cards', + }, + { + label: '🎯 Team Goals', + prompt: 'show team goals', + desc: 'Unrolled objectives list', + }, + { + label: '💬 Feedback Board', + prompt: 'show feedback board', + desc: 'Review cards with ratings', + }, + { + label: '⭐ Competency Panel', + prompt: 'show competency panel', + desc: 'Metrics & stats summary', + }, + ].map(btn => ( + + ))} +
+
+ +
+ Template Inference Format +
+ Click the ℹ️ Inspect button + on any turn to view the raw LLM Express DSL and expanded JSON. +
+
+ + {/* Chat Feed */} +
+
+ {feed.length === 0 && ( +
+
+ + auto_awesome + +
+

+ A2UI Templates Explorer +

+

+ Click a preset or select a suggested prompt below to observe static and dynamic + templates expanded server-side into standard A2UI primitives. +

+ +
+
+ Suggested Prompts +
+
+ {[ + { + icon: 'lock', + text: 'Show verified salary for Marcus Vance', + badge: 'Dynamic', + }, + { + icon: 'person', + text: 'Show user profile for Alice Smith', + badge: 'Template', + }, + { + icon: 'flag', + text: 'Show team goals for Core Protocol Engineering', + badge: 'Template', + }, + { + icon: 'reviews', + text: 'Show feedback board for Frontend Guild', + badge: 'Template', + }, + { + icon: 'groups', + text: 'Show team roster with Core Architecture', + badge: 'Template', + }, + { + icon: 'monitoring', + text: 'Show user evaluation for Alice Smith', + badge: 'Composite', + }, + ].map(chip => ( + + ))} +
+
+
+ )} + + {feed.map(item => { + const targetSurfaceId = + item.surfaceId || + item.messages?.find((m: any) => m.createSurface?.surfaceId)?.createSurface + ?.surfaceId || + item.messages?.find((m: any) => m.updateComponents?.surfaceId)?.updateComponents + ?.surfaceId; + const surface = targetSurfaceId + ? chatProcessor.model.getSurface(targetSurfaceId) + : undefined; + const isInspectorOpen = activeInspector === item.id; + const hasInspectionData = Boolean( + item.raw || (item.messages && item.messages.length > 0), + ); + + return ( +
+ {item.type === 'user' ? ( +
+ {item.text} +
+ ) : ( +
+
+ {item.text && ( +
+ {item.text} +
+ )} + +
+ {item.metrics && ( +
+ + ⏱️ + {item.metrics.latency}s + + + {item.metrics.thinkingTokens !== undefined && + item.metrics.thinkingTokens > 0 && ( + <> + + + 🧠 + {item.metrics.thinkingTokens} think + + + )} + + {item.metrics.outputTokens !== undefined && + item.metrics.outputTokens > 0 && ( + <> + + + 📝 + {item.metrics.outputTokens} out + + + )} +
+ )} + + {hasInspectionData && ( + + )} +
+
+ + {/* Inspector Drawer */} + {isInspectorOpen && ( +
+
+
+ + +
+ + +
+ +
+ {inspectorTab === 'express' ? ( +
+                                  {item.raw || '// No raw Express DSL received'}
+                                
+ ) : ( +
+                                  {JSON.stringify(item.messages || [], null, 2)}
+                                
+ )} +
+
+ )} + + {/* Rendered A2UI Surface */} + {surface ? ( +
+ +
+ ) : null} +
+ )} +
+ ); + })} + + {loading && ( +
+ + progress_activity + + Expanding template... +
+ )} +
+
+ + {/* Input Bar */} +
+ setInput(e.target.value)} + onKeyDown={e => e.key === 'Enter' && sendPrompt(input)} + placeholder="Type a template prompt (e.g. 'Show verified salary' or 'Show user profile')..." + disabled={loading} + style={{ + flex: 1, + padding: '14px 18px', + borderRadius: '12px', + border: '1px solid #cbd5e1', + fontSize: '14px', + outline: 'none', + transition: 'border-color 0.15s ease', + }} + onFocus={e => (e.target.style.borderColor = '#2563eb')} + onBlur={e => (e.target.style.borderColor = '#cbd5e1')} + /> + +
+
+
+ ) : ( + /* Template Library View */ +
+ {/* Library Sidebar List */} +
+
+

+ Registered Templates +

+

+ Inspect static declarative templates and dynamic server resolvers. +

+
+ + {templates.map(tmpl => { + const isSelected = tmpl.templateId === selectedTemplateId; + const paramCount = Object.keys(tmpl.parameters || {}).length; + const compCount = (tmpl.components || []).length; + + return ( + + ); + })} +
+ + {/* Studio Content */} + {selectedTemplate ? ( + selectedTemplate.isDynamic ? ( + /* Dynamic Template 3-Stage Inspector Studio */ +
+ {/* Header Banner */} +
+
+
+

+ {selectedTemplate.templateId} +

+ + ⚡ Dynamic Server Resolver + +
+

+ {selectedTemplate.description} +

+
+ + {/* Stage Switcher */} +
+ {[ + {id: 'input', label: '1. Input Interface'}, + { + id: 'layout', + label: selectedTemplate.isProgrammatic + ? '2. Python AST Generator' + : '2. Static Blueprint', + }, + {id: 'resolved', label: '3. Resolved Output'}, + ].map(tab => ( + + ))} +
+
+ + {/* 3-Stage Body */} +
+ {/* Left Column: Interactive Stages */} +
+ {dynamicTab === 'input' && ( +
+
+

+ Step 1: Simple LLM Input Interface +

+

+ The LLM generates only simple identifiers. Confidential figures are + never exposed in prompt context. +

+
+ + {/* Input Selector Form */} + {selectedTemplate.templateId === 'PayrollSummary' ? ( +
+
+ + { + setPayrollDept(e.target.value); + handleResolveDynamicTemplate({department: e.target.value}); + }} + style={{ + width: '100%', + padding: '8px 12px', + borderRadius: '8px', + border: '1px solid #cbd5e1', + fontSize: '13px', + boxSizing: 'border-box', + }} + /> +
+ + + +
+
+ Generated Express DSL by LLM: +
+
+                                {`\nroot = PayrollSummary("${payrollDept}", ${payrollIncludeBonus})\n`}
+                              
+
+
+ ) : ( +
+ + + +
+
+ Generated Express DSL by LLM: +
+
+                                {`\nroot = EmployeeSalaryCard("${selectedDynamicEmpId}")\n`}
+                              
+
+
+ )} + +
+ + + {selectedTemplate.isProgrammatic + ? '✓ Python execution engine active' + : '✓ Server resolver connected'} + +
+
+ )} + + {dynamicTab === 'layout' && ( +
+
+

+ {selectedTemplate.isProgrammatic + ? 'Step 2: Python Render Function (Programmatic AST Generator)' + : 'Step 2: Underlying Layout Template (Static Blueprint)'} +

+

+ {selectedTemplate.isProgrammatic + ? 'This template is generated directly by a Python render function using loops, conditionals, and math to construct the component AST.' + : 'The visual layout is declared once in YAML (salary_card.yaml). Parameter placeholders like baseSalary and annualBonus are populated by the server callback.'} +

+
+ +
+                          {selectedTemplate.isProgrammatic
+                            ? selectedTemplate.renderSource || '# Python render function'
+                            : selectedTemplate.layoutTemplateYaml ||
+                              selectedTemplate.yamlContent ||
+                              ''}
+                        
+
+ )} + + {dynamicTab === 'resolved' && ( +
+
+

+ Step 3: Server-Resolved Injected Data +

+

+ Live record retrieved from the internal HR database for{' '} + {selectedDynamicEmpId}. +

+
+ +
+                          {JSON.stringify(dynamicResolvedData || {}, null, 2)}
+                        
+
+ )} +
+ + {/* Right Column: Live Inflated Preview */} +
+
+

+ Inflated Output Preview +

+ + ✓ Live Inflated + +
+ +
+ {(() => { + const dynSurface = + libraryProcessor.model.getSurface( + `preview_${selectedTemplate.templateId}_${selectedDynamicEmpId}`, + ) || + libraryProcessor.model.getSurface( + `preview_${selectedTemplate.templateId}`, + ); + return dynSurface ? ( + + ) : ( +
+ No preview surface available for this template. +
+ ); + })()} +
+
+
+
+ ) : ( + /* Standard Static Template Studio */ +
+ {/* Left: Live Inflated Preview */} +
+
+
+

+ Inflated UI Preview +

+

+ Rendered via @a2ui/react using declared sampleData +

+
+ + ✓ Live Inflated + +
+ +
+ {libraryProcessor.model.getSurface(`preview_${selectedTemplate.templateId}`) ? ( + + ) : ( +
+ No preview surface available for this template. +
+ )} +
+ + {selectedTemplate.sampleData && ( +
+

+ Sample Data Inputs +

+
+                        {JSON.stringify(selectedTemplate.sampleData, null, 2)}
+                      
+
+ )} +
+ + {/* Right: Code with Line Numbers & Monospace Font */} +
+
+
+

+ Template Declaration (YAML) +

+

+ Parameterized YAML layout definition +

+
+ + +
+ +
+
+ {selectedTemplate.templateId.toLowerCase()}.yaml + YAML Schema draft 2020-12 +
+ +
+ {(() => { + const yamlText = selectedTemplate.yamlContent || ''; + const lines = yamlText.split('\n'); + + return ( + <> +
+ {lines.map((_, idx) => ( +
{idx + 1}
+ ))} +
+ +
+ {lines.map((line, idx) => ( +
{line || ' '}
+ ))} +
+ + ); + })()} +
+
+
+
+ ) + ) : ( +
+ {libraryLoading ? 'Loading templates...' : 'No templates available.'} +
+ )} +
+ )} +
+ ); +} diff --git a/samples/community/templates/client/src/main.tsx b/samples/community/templates/client/src/main.tsx new file mode 100644 index 0000000000..6d92e4f429 --- /dev/null +++ b/samples/community/templates/client/src/main.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , +); diff --git a/samples/community/templates/client/tsconfig.json b/samples/community/templates/client/tsconfig.json new file mode 100644 index 0000000000..0426f7bb8e --- /dev/null +++ b/samples/community/templates/client/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/samples/community/templates/client/vite.config.ts b/samples/community/templates/client/vite.config.ts new file mode 100644 index 0000000000..95c8497c8d --- /dev/null +++ b/samples/community/templates/client/vite.config.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + open: false, + }, +}); diff --git a/samples/community/templates/pyproject.toml b/samples/community/templates/pyproject.toml new file mode 100644 index 0000000000..c2dd8b8164 --- /dev/null +++ b/samples/community/templates/pyproject.toml @@ -0,0 +1,30 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[project] +name = "a2ui-templates-demo-server" +version = "0.1.0" +description = "A2UI Templates Community Demo Server" +requires-python = ">=3.10" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn>=0.30.0", + "google-genai>=1.0.0", + "a2ui-agent-sdk", + "a2ui-core" +] + +[tool.uv.sources] +a2ui-agent-sdk = { path = "../../../agent_sdks/python/a2ui_agent", editable = true } +a2ui-core = { path = "../../../agent_sdks/python/a2ui_core", editable = true } diff --git a/samples/community/templates/server.py b/samples/community/templates/server.py new file mode 100644 index 0000000000..a2c56e1b1e --- /dev/null +++ b/samples/community/templates/server.py @@ -0,0 +1,642 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Concise FastAPI server demonstrating A2UI Static and Dynamic Templates with Agent SDK.""" + +from __future__ import annotations + +import glob +import inspect +import json +import os +from pathlib import Path +import time +from typing import Any, Dict, List +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from google import genai +from google.genai import types +from pydantic import BaseModel + +from a2ui.inference_formats.experimental.template import ( + Template, + StaticTemplate, + DynamicTemplate, + Param, + ParamType, + TemplateInferenceFormat, +) + +app = FastAPI(title="A2UI Templates Community Demo Server") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_headers=["*"], + allow_methods=["*"], +) + +MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-flash-latest") +BASIC_CATALOG_ID = "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" + +# Mock Secure HR / Compensation Database +EMPLOYEE_COMPENSATION_DB = { + "emp_101": { + "employeeName": "Dr. Elena Vance", + "role": "Principal Systems Architect", + "baseSalary": "$215,000", + "annualBonus": "$45,000", + "equity": "3,500 RSUs", + "clearanceLevel": "Level 5 - Confidential", + "verifiedAt": "2026-08-13", + }, + "emp_102": { + "employeeName": "Marcus Vance", + "role": "Streaming & Protocols Lead", + "baseSalary": "$195,000", + "annualBonus": "$38,000", + "equity": "2,800 RSUs", + "clearanceLevel": "Level 4 - Confidential", + "verifiedAt": "2026-08-13", + }, + "emp_103": { + "employeeName": "Aria Chen", + "role": "Head of Design Systems", + "baseSalary": "$205,000", + "annualBonus": "$42,000", + "equity": "3,100 RSUs", + "clearanceLevel": "Level 5 - Confidential", + "verifiedAt": "2026-08-13", + }, + "emp_104": { + "employeeName": "Liam Kjell", + "role": "Senior Framework Engineer", + "baseSalary": "$180,000", + "annualBonus": "$32,000", + "equity": "2,200 RSUs", + "clearanceLevel": "Level 3 - Internal", + "verifiedAt": "2026-08-13", + }, +} + + +def fetch_employee_compensation(employeeId: str) -> Dict[str, Any]: + """Fetches verified confidential compensation package from internal HR database.""" + if employeeId not in EMPLOYEE_COMPENSATION_DB: + raise ValueError( + f"Employee ID '{employeeId}' not found in HR compensation records." + f" Available: {list(EMPLOYEE_COMPENSATION_DB.keys())}" + ) + return EMPLOYEE_COMPENSATION_DB[employeeId] + + +def render_payroll_summary( + department: str = "Global Engineering", includeBonus: bool = True +) -> Dict[str, Any]: + """Programmatic dynamic template: performs Python math, loops, formatting, and builds an AST table.""" + total_base = 0 + total_bonus = 0 + rows: List[Dict[str, Any]] = [] + + for emp_id, record in EMPLOYEE_COMPENSATION_DB.items(): + base_int = int(record["baseSalary"].replace("$", "").replace(",", "")) + bonus_int = int(record["annualBonus"].replace("$", "").replace(",", "")) + total_base += base_int + total_bonus += bonus_int + + cols = [ + {"component": "Text", "text": record["employeeName"], "variant": "body"}, + {"component": "Text", "text": record["role"], "variant": "caption"}, + {"component": "Text", "text": record["baseSalary"], "variant": "body"}, + ] + if includeBonus: + cols.append( + {"component": "Text", "text": record["annualBonus"], "variant": "body"} + ) + + rows.append({ + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": cols, + }) + rows.append({"component": "Divider", "axis": "horizontal"}) + + # Header Row + header_cols = [ + {"component": "Text", "text": "Employee", "variant": "caption"}, + {"component": "Text", "text": "Role", "variant": "caption"}, + {"component": "Text", "text": "Base Salary", "variant": "caption"}, + ] + if includeBonus: + header_cols.append( + {"component": "Text", "text": "Annual Bonus", "variant": "caption"} + ) + + # Total Row + total_cols = [ + {"component": "Text", "text": "TOTAL PAYROLL", "variant": "h4"}, + { + "component": "Text", + "text": f"{len(EMPLOYEE_COMPENSATION_DB)} Employees", + "variant": "caption", + }, + {"component": "Text", "text": f"${total_base:,}", "variant": "h4"}, + ] + if includeBonus: + total_cols.append( + {"component": "Text", "text": f"${total_bonus:,}", "variant": "h4"} + ) + + total_budget = total_base + (total_bonus if includeBonus else 0) + + return { + "component": "Card", + "child": { + "component": "Column", + "children": [ + { + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": [ + { + "component": "Row", + "align": "center", + "children": [ + {"component": "Icon", "name": "lock"}, + { + "component": "Text", + "text": ( + f"Payroll & Compensation Summary: {department}" + ), + "variant": "h3", + }, + ], + }, + { + "component": "Text", + "text": "Confidential HR Record", + "variant": "caption", + }, + ], + }, + {"component": "Divider", "axis": "horizontal"}, + { + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": header_cols, + }, + {"component": "Divider", "axis": "horizontal"}, + *rows, + { + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": total_cols, + }, + {"component": "Divider", "axis": "horizontal"}, + { + "component": "Row", + "justify": "spaceBetween", + "align": "center", + "children": [ + { + "component": "Text", + "text": ( + "🔒 Computed live by server Python execution engine" + ), + "variant": "caption", + }, + { + "component": "Text", + "text": f"Total Budget: ${total_budget:,}", + "variant": "caption", + }, + ], + }, + ], + }, + } + + +def load_templates() -> List[Any]: + """Loads all static templates and registers dynamic resolver templates.""" + current_file = Path(__file__).resolve() + templates_dir = current_file.parent / "templates" + templates_pattern = str(templates_dir / "*.yaml") + + templates_list = [] + salary_layout = None + + for path in glob.glob(templates_pattern): + loaded_templates = StaticTemplate.from_yaml_file(path) + for tmpl in loaded_templates: + if tmpl.template_id == "SalaryCard": + salary_layout = tmpl + else: + templates_list.append(tmpl) + + if salary_layout is not None: + # Register DynamicTemplate for EmployeeSalaryCard (Data Binding Mode) + dynamic_salary = DynamicTemplate( + version="0.1", + template_id="EmployeeSalaryCard", + resolver=fetch_employee_compensation, + layout=salary_layout, + description=( + "Secure verified employee compensation card. Pass only the" + " employeeId ('emp_101', 'emp_102', 'emp_103', 'emp_104');" + " compensation data is securely fetched server-side from the" + " HR database." + ), + sample_data={"employeeId": "emp_101"}, + ) + templates_list.append(dynamic_salary) + + # Register DynamicTemplate for PayrollSummary (Programmatic Render Mode) + payroll_template = DynamicTemplate( + version="0.1", + template_id="PayrollSummary", + render=render_payroll_summary, + description=( + "Programmatic dynamic template that aggregates company payroll, sums" + " employee base salaries and bonuses using server Python logic, and builds" + " an interactive summary table. Pass department (default 'Engineering') and" + " includeBonus (default True)." + ), + sample_data={"department": "Global Engineering", "includeBonus": True}, + ) + templates_list.append(payroll_template) + + return templates_list + + +templates = load_templates() +format_instance = TemplateInferenceFormat( + templates=templates, + surface_id="main", + version="0.9.1", +) + +ROLE_DESCRIPTION = """You are an A2UI interface assistant. When helpful, respond with visual UI using the compact A2UI Express DSL inside `` tags. + +Select and present only the UI components that are directly relevant to the user's request. Depending on the query, this may be a single template, a standard primitive, or a custom composed layout that you invent to address the query. + +You can compose high-level templates and primitive components together: +- Layout & Containers: `Column`, `Row`, `Card`, `SectionCard(title, description, headerAction, children)`, `TwoColumnLayout(headerChild, leftChildren, rightChildren)`. +- Reusable & Dynamic Templates: + - `UserProfile(userId, userName, role)` for individual identity cards. + - `EmployeeSalaryCard(employeeId)` for verified employee compensation. Pass only the employeeId (e.g. "emp_101" for Dr. Elena Vance, "emp_102" for Marcus Vance, "emp_103" for Aria Chen, "emp_104" for Liam Kjell). Confidential compensation data is securely resolved server-side from the HR database. + - `TeamMemberKnowledgePanel(userName, role, experienceYears, completedTasks)` for stats and skill summaries. + - `TeamGoalList(teamName, goals)` or `GoalItem(title, priority, targetDate)` for objective tracking. + - `TeamFeedbackBoard(teamName, feedbacks)` or `FeedbackItem(author, note, rating)` for reviews and retrospectives. + - `TeamCard(teamName, members)` and `TeamRoster(directoryTitle, children)` for organizational hierarchies. + +For complex queries requiring multiple sections (such as a performance review or project status), you can invent appropriate composite layouts—for instance, grouping a `TeamMemberKnowledgePanel`, `TeamFeedbackBoard`, and `TeamGoalList` within a `Column` or `TwoColumnLayout`.""" + +SYSTEM_PROMPT = format_instance.prompt_generator.generate( + role_description=ROLE_DESCRIPTION, + include_schema=True, +) + + +class ChatRequest(BaseModel): + prompt: str + surfaceId: str = "surface_1" + conversationId: str = "default_conv" + + +class DynamicResolveRequest(BaseModel): + params: Dict[str, Any] + + +PRESET_RESPONSES = { + "show user profile": ( + """ + + root = UserProfile("usr_101", "Alice Smith", "Lead Architect") + + """ + ), + "show verified salary": ( + """ + + root = EmployeeSalaryCard("emp_102") + + """ + ), + "show payroll summary": ( + """ + + root = PayrollSummary("Global Engineering", true) + + """ + ), + "show user evaluation": ( + """ + + knowledge = TeamMemberKnowledgePanel("Alice Smith", "Lead Systems Architect", 9, 142) + feedbacks = TeamFeedbackBoard("Peer Reviews & Feedback", [ + {author: "Dr. Elena Vance", note: "Exceptional architecture design and synchronous template engine.", rating: 5}, + {author: "Marcus Vance", note: "Great mentor on A2UI streaming and component catalogs.", rating: 5} + ]) + goals = TeamGoalList("2026 Objectives", [ + {title: "Finalize A2UI Protocol Specification", priority: "High", targetDate: "2026-09-30"}, + {title: "Publish Community Template Studio", priority: "High", targetDate: "2026-10-15"} + ]) + root = Column([knowledge, feedbacks, goals]) + + """ + ), + "show team roster": ( + """ + + team1 = TeamCard("Core Architecture", [ + {userId: "u1", userName: "Dr. Elena Vance", role: "Principal Architect"}, + {userId: "u2", userName: "Marcus Vance", role: "Streaming Lead"} + ]) + team2 = TeamCard("Design Systems", [ + {userId: "u3", userName: "Aria Chen", role: "Head of Design"}, + {userId: "u4", userName: "Liam Kjell", role: "Senior Engineer"} + ]) + root = TeamRoster("Organization Directory", [team1, team2]) + + """ + ), + "show team goals": ( + """ + + root = TeamGoalList("Core Protocol Engineering", [ + {title: "Deliver synchronous template expansion engine", priority: "High", targetDate: "2026-08-30"}, + {title: "Redesign templates for Basic Catalog", priority: "High", targetDate: "2026-08-15"}, + {title: "Simplify community demo architectures", priority: "Medium", targetDate: "2026-09-01"} + ]) + + """ + ), + "show feedback board": ( + """ + + root = TeamFeedbackBoard("Frontend & Protocols Guild", [ + {author: "Dr. Elena Vance", note: "Synchronous template expansion eliminated all streaming race conditions.", rating: 5}, + {author: "Marcus Vance", note: "Standard Basic Catalog components ensure 100% cross-renderer compatibility.", rating: 5} + ]) + + """ + ), + "show competency panel": ( + """ + + root = TeamMemberKnowledgePanel("Alice Smith", "Lead Systems Architect", 9, 142) + + """ + ), +} + + +@app.get("/templates") +@app.get("/api/templates") +def list_templates(): + res = [] + for t in templates: + t_dict = t.to_dict() + t_dict["yamlContent"] = t.to_yaml() + sample_params = t.sample_data or {} + try: + expanded_components = format_instance.processor.expand_template( + "root", t.template_id, sample_params + ) + sample_messages = [ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": f"preview_{t.template_id}", + "catalogId": BASIC_CATALOG_ID, + }, + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": f"preview_{t.template_id}", + "components": expanded_components, + }, + }, + ] + except Exception: + sample_messages = [] + t_dict["sampleMessages"] = sample_messages + + if getattr(t, "is_dynamic", False): + dynamic_tmpl: DynamicTemplate = t # type: ignore + t_dict["isDynamic"] = True + if getattr(dynamic_tmpl, "render_fn", None) is not None: + t_dict["isProgrammatic"] = True + try: + t_dict["renderSource"] = inspect.getsource(dynamic_tmpl.render_fn) + except Exception: + t_dict["renderSource"] = "" + if dynamic_tmpl.layout is not None: + t_dict["isDataBinding"] = True + t_dict["layoutTemplate"] = dynamic_tmpl.layout.to_dict() + t_dict["layoutTemplateYaml"] = dynamic_tmpl.layout.to_yaml() + # Run resolver on sampleData to show resolved state + try: + t_dict["resolvedData"] = dynamic_tmpl.resolve(sample_params) + except Exception: + t_dict["resolvedData"] = {} + if dynamic_tmpl.template_id == "EmployeeSalaryCard": + t_dict["availablePresets"] = [ + {"label": f"{v['employeeName']} ({k})", "value": k} + for k, v in EMPLOYEE_COMPENSATION_DB.items() + ] + + res.append(t_dict) + return res + + +@app.post("/templates/{template_id}/resolve") +@app.post("/api/templates/{template_id}/resolve") +def resolve_template(template_id: str, req: DynamicResolveRequest): + tmpl = format_instance.processor.templates.get(template_id) + if not tmpl: + raise HTTPException(status_code=404, detail="Template not found") + + try: + expanded_components = format_instance.processor.expand_template( + "root", template_id, req.params + ) + sample_messages = [ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": f"preview_{template_id}", + "catalogId": BASIC_CATALOG_ID, + }, + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": f"preview_{template_id}", + "components": expanded_components, + }, + }, + ] + resolved_data = {} + if getattr(tmpl, "is_dynamic", False): + if getattr(tmpl, "render_fn", None) is not None: + resolved_data = { + "execution": "Python Programmatic Render Function", + "generatedComponentCount": len(expanded_components), + "appliedParams": req.params, + } + else: + resolved_data = tmpl.resolve(req.params) # type: ignore + + return { + "expandedComponents": expanded_components, + "sampleMessages": sample_messages, + "resolvedData": resolved_data, + } + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@app.post("/interact") +@app.post("/api/chat") +async def chat(req: ChatRequest): + start_time = time.perf_counter() + prompt_lower = req.prompt.strip().lower() + + # 1. Preset shortcut responses for instant offline evaluation + if prompt_lower in PRESET_RESPONSES: + dsl = PRESET_RESPONSES[prompt_lower] + target_format = TemplateInferenceFormat( + templates=templates, + surface_id=req.surfaceId, + version="0.9.1", + ) + parts = target_format.parser.parse_response(dsl) + messages = parts[0].a2ui_json if parts and parts[0].a2ui_json else [] + latency = round(time.perf_counter() - start_time, 3) + return { + "messages": messages, + "raw": dsl.strip(), + "text": f"Here is the rendered {req.prompt}:", + "surfaceId": req.surfaceId, + "metrics": { + "latency": latency, + "thinkingTokens": 0, + "outputTokens": len(dsl.split()), + "isPreset": True, + }, + } + + # 2. Live Gemini inference if API key is provided + api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY") + if api_key: + try: + client = genai.Client(api_key=api_key) + response = await client.aio.models.generate_content( + model=MODEL_NAME, + contents=[req.prompt], + config=types.GenerateContentConfig( + system_instruction=SYSTEM_PROMPT, + response_mime_type="text/plain", + ), + ) + latency = round(time.perf_counter() - start_time, 2) + raw_text = response.text or "" + target_format = TemplateInferenceFormat( + templates=templates, + surface_id=req.surfaceId, + version="0.9.1", + ) + parts = target_format.parser.parse_response(raw_text) + + messages = [] + text_parts = [] + for part in parts: + if part.text: + text_parts.append(part.text) + if part.a2ui_json: + messages.extend(part.a2ui_json) + + thinking_tokens = 0 + candidates_tokens = 0 + if response.usage_metadata: + thinking_tokens = ( + getattr(response.usage_metadata, "thoughts_token_count", 0) or 0 + ) + candidates_tokens = ( + getattr(response.usage_metadata, "candidates_token_count", 0) or 0 + ) + + actual_surface_id = req.surfaceId + for msg in messages: + if isinstance(msg, dict): + if "createSurface" in msg and "surfaceId" in msg["createSurface"]: + actual_surface_id = msg["createSurface"]["surfaceId"] + break + elif ( + "updateComponents" in msg + and "surfaceId" in msg["updateComponents"] + ): + actual_surface_id = msg["updateComponents"]["surfaceId"] + break + + return { + "messages": messages, + "raw": raw_text.strip(), + "text": "\n".join(text_parts).strip() or "UI generated successfully.", + "surfaceId": actual_surface_id, + "metrics": { + "latency": latency, + "thinkingTokens": thinking_tokens, + "outputTokens": candidates_tokens, + "isPreset": False, + }, + } + except Exception as e: + return { + "messages": [], + "raw": f"Error: {str(e)}", + "text": f"Error generating template UI: {str(e)}", + "surfaceId": req.surfaceId, + "metrics": { + "latency": round(time.perf_counter() - start_time, 2), + "thinkingTokens": 0, + "outputTokens": 0, + "isPreset": False, + }, + } + + # 3. Fallback when no Gemini API key is configured + return { + "messages": [], + "raw": "", + "text": ( + "Gemini API key is not configured on the server. Please click one of the" + " preset buttons above or set the GEMINI_API_KEY environment variable." + ), + "surfaceId": req.surfaceId, + "metrics": { + "latency": 0.0, + "thinkingTokens": 0, + "outputTokens": 0, + "isPreset": True, + }, + } diff --git a/samples/community/templates/templates/feedback_item.yaml b/samples/community/templates/templates/feedback_item.yaml new file mode 100644 index 0000000000..bb170c2333 --- /dev/null +++ b/samples/community/templates/templates/feedback_item.yaml @@ -0,0 +1,46 @@ +version: "0.1" +templateId: FeedbackItem +description: Card showing feedback note, author, and rating. + +parameters: + author: + type: string + title: Author Name + description: The name of the colleague or customer providing feedback. + note: + type: string + title: Feedback Note + description: The written feedback or retrospective comment. + rating: + type: number + title: Feedback Rating + description: Score from 1 to 5. + minimum: 1 + maximum: 5 + default: 5 + +layout: + component: Card + child: + component: Column + children: + - component: Text + text: ${note} + variant: body + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${author} + variant: caption + - component: Text + text: "Rating: ${rating}/5" + variant: caption + +sampleData: + author: Dr. Elena Vance + note: A2UI templates are fast and easy to compose. + rating: 5 diff --git a/samples/community/templates/templates/goal_item.yaml b/samples/community/templates/templates/goal_item.yaml new file mode 100644 index 0000000000..a9009555b0 --- /dev/null +++ b/samples/community/templates/templates/goal_item.yaml @@ -0,0 +1,59 @@ +version: "0.1" +templateId: GoalItem +description: Card showing an individual objective with priority, title, target date, and action button. + +parameters: + title: + type: string + title: Goal Title + description: Summary title of the objective. + priority: + type: enum + title: Priority Level + description: Urgency rating for the objective. + values: + - High + - Medium + - Low + default: Medium + targetDate: + type: string + title: Target Date + description: Target completion date in YYYY-MM-DD format. + default: "2026-12-31" + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "Priority: ${priority}" + variant: caption + - component: Icon + name: star + - component: Text + text: ${title} + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "Due: ${targetDate}" + variant: caption + - component: Button + child: + component: Text + text: View Details + +sampleData: + title: Launch A2UI SDK v1.0 + priority: High + targetDate: "2026-09-30" diff --git a/samples/community/templates/templates/salary_card.yaml b/samples/community/templates/templates/salary_card.yaml new file mode 100644 index 0000000000..f4b847e746 --- /dev/null +++ b/samples/community/templates/templates/salary_card.yaml @@ -0,0 +1,116 @@ +version: "0.1" +templateId: SalaryCard +description: Layout card for employee compensation package with security verification badge. + +parameters: + employeeName: + type: string + title: Employee Full Name + description: Full name of the verified employee. + role: + type: string + title: Job Title + description: Official company role. + baseSalary: + type: string + title: Base Salary + description: Annual base compensation. + annualBonus: + type: string + title: Annual Bonus + description: Target annual incentive bonus. + equity: + type: string + title: Equity Grants + description: Stock units or RSU package. + clearanceLevel: + type: string + title: Security Clearance + description: Confidentiality level. + default: "Level 4 - Confidential" + verifiedAt: + type: string + title: Verification Date + description: Timestamp of record retrieval. + default: "2026-08-13" + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Row + align: center + children: + - component: Icon + name: lock + - component: Text + text: "Verified Compensation" + variant: caption + - component: Text + text: ${clearanceLevel} + variant: caption + - component: Column + children: + - component: Text + text: ${employeeName} + variant: h3 + id: name_txt + - component: Text + text: ${role} + variant: body + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + children: + - component: Column + children: + - component: Text + text: "Base Salary" + variant: caption + - component: Text + text: ${baseSalary} + variant: h4 + id: sal_val + - component: Column + children: + - component: Text + text: "Annual Bonus" + variant: caption + - component: Text + text: ${annualBonus} + variant: h4 + - component: Column + children: + - component: Text + text: "Equity Grants" + variant: caption + - component: Text + text: ${equity} + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: "🔒 Fetched live from secure HR database" + variant: caption + - component: Text + text: "Verified: ${verifiedAt}" + variant: caption + +sampleData: + employeeName: Dr. Elena Vance + role: Principal Systems Architect + baseSalary: "$215,000" + annualBonus: "$45,000" + equity: "3,500 RSUs" + clearanceLevel: "Level 5 - Confidential" + verifiedAt: "2026-08-13" diff --git a/samples/community/templates/templates/section_card.yaml b/samples/community/templates/templates/section_card.yaml new file mode 100644 index 0000000000..7511911d03 --- /dev/null +++ b/samples/community/templates/templates/section_card.yaml @@ -0,0 +1,52 @@ +version: "0.1" +templateId: SectionCard +description: Standard container card with title, description, optional action, and child components. + +parameters: + title: + type: string + title: Section Title + description: Heading text displayed at the top of the section card. + description: + type: string + title: Section Description + description: Subordinate descriptive text beneath the title. + default: "" + headerAction: + type: child + title: Header Action Component + description: Single child component placed in the right side of the header. + children: + type: children + title: Section Children + description: Child component IDs rendered in the section body. + default: [] + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${title} + variant: h3 + - component: Column + id: action_slot + children: ${headerAction} + - component: Text + text: ${description} + variant: caption + - component: Divider + axis: horizontal + - component: Column + id: body_container + children: ${children} + +sampleData: + title: Protocol Overview + description: High-level summary of the streaming architecture. + children: [] diff --git a/samples/community/templates/templates/team_card.yaml b/samples/community/templates/templates/team_card.yaml new file mode 100644 index 0000000000..ec751df98d --- /dev/null +++ b/samples/community/templates/templates/team_card.yaml @@ -0,0 +1,68 @@ +version: "0.1" +templateId: TeamCard +description: Card showing team header and unrolled member cards. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team. + members: + type: array + title: Team Members List + description: Array of team member objects. + items: + type: object + title: Team Member + properties: + userId: + type: string + title: User ID + description: Unique identifier of the user. + userName: + type: string + title: User Name + description: Full name of the user. + role: + type: string + title: Role Name + description: Role or title of the user. + default: Member + required: + - userId + - userName + +layout: + component: Card + child: + component: Column + children: + - component: Row + justify: spaceBetween + align: center + children: + - component: Text + text: ${teamName} + variant: h3 + - component: Icon + name: person + - component: Divider + axis: horizontal + - component: Column + children: + loop: + param: members + template: UserProfile + +sampleData: + teamName: Antigravity Devs + members: + - userId: usr_101 + userName: Alice Smith + role: Lead Architect + - userId: usr_102 + userName: Bob Jones + role: Senior Engineer + - userId: usr_103 + userName: Charlie Brown + role: Product Manager diff --git a/samples/community/templates/templates/team_feedback_board.yaml b/samples/community/templates/templates/team_feedback_board.yaml new file mode 100644 index 0000000000..07efe56f33 --- /dev/null +++ b/samples/community/templates/templates/team_feedback_board.yaml @@ -0,0 +1,64 @@ +version: "0.1" +templateId: TeamFeedbackBoard +description: Feedback board showing team header and feedback items. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team whose feedback board is being rendered. + feedbacks: + type: array + title: Feedbacks List + description: Array of feedback objects with author, note, and rating. + items: + type: object + title: Feedback Review + properties: + author: + type: string + title: Author Name + description: Full name of the person giving feedback. + note: + type: string + title: Feedback Note + description: Textual comment or recommendation. + rating: + type: number + title: Rating Score + minimum: 1 + maximum: 5 + default: 5 + required: + - author + - note + +layout: + component: Column + children: + - component: Card + child: + component: Row + align: center + children: + - component: Icon + name: mail + - component: Text + text: "Feedback & Retrospective: ${teamName}" + variant: h2 + - component: Column + id: feedbacks_container + children: + loop: + param: feedbacks + template: FeedbackItem + +sampleData: + teamName: Streaming & Protocols Guild + feedbacks: + - author: Dr. Elena Vance + note: Splitting createSurface and updateComponents cleanly unblocked sequential stream processing. + rating: 5 + - author: Marcus Vance + note: Unrolling nested lists statically in Python enables instant frontend mounting with zero runtime recursion. + rating: 5 diff --git a/samples/community/templates/templates/team_goal_list.yaml b/samples/community/templates/templates/team_goal_list.yaml new file mode 100644 index 0000000000..e2afc5fccc --- /dev/null +++ b/samples/community/templates/templates/team_goal_list.yaml @@ -0,0 +1,65 @@ +version: "0.1" +templateId: TeamGoalList +description: List of team goals with header banner. + +parameters: + teamName: + type: string + title: Team Name + description: The display name of the team whose goals are being listed. + goals: + type: array + title: Goals List + description: Array of goal objects with title, priority, and targetDate. + items: + type: object + title: Goal Definition + properties: + title: + type: string + title: Goal Title + description: Summary title of the objective + priority: + type: enum + title: Priority Level + values: + - High + - Medium + - Low + default: Medium + targetDate: + type: string + title: Target Date + description: Target completion date (YYYY-MM-DD) + required: + - title + +layout: + component: Column + children: + - component: Card + child: + component: Row + align: center + children: + - component: Icon + name: star + - component: Text + text: "Strategic Objectives: ${teamName}" + variant: h2 + - component: Column + id: goals_container + children: + loop: + param: goals + template: GoalItem + +sampleData: + teamName: A2UI Core Team + goals: + - title: Implement bidirectional child/children container resolution + priority: High + targetDate: "2026-07-15" + - title: Upgrade all templates to standard Basic Catalog components + priority: High + targetDate: "2026-07-10" diff --git a/samples/community/templates/templates/team_member_knowledge_panel.yaml b/samples/community/templates/templates/team_member_knowledge_panel.yaml new file mode 100644 index 0000000000..925788ea82 --- /dev/null +++ b/samples/community/templates/templates/team_member_knowledge_panel.yaml @@ -0,0 +1,78 @@ +version: "0.1" +templateId: TeamMemberKnowledgePanel +description: Card showing competency panel with user experience years and completed task count. + +parameters: + userName: + type: string + title: User Display Name + description: The full name of the team member. + role: + type: string + title: Role Name + description: The operational role of the team member. + experienceYears: + type: integer + title: Years of Experience + description: The number of years of professional experience. + minimum: 0 + completedTasks: + type: integer + title: Completed Tasks Count + description: The count of completed tasks or tickets. + minimum: 0 + +layout: + component: Card + child: + component: Column + children: + - component: Row + align: center + children: + - component: Icon + name: check + - component: Text + text: "Competency: ${userName}" + variant: h4 + - component: Divider + axis: horizontal + - component: Row + justify: spaceBetween + children: + - component: Column + align: center + children: + - component: Text + text: Role + variant: caption + - component: Text + text: ${role} + variant: body + - component: Column + align: center + children: + - component: Text + text: Experience + variant: caption + - component: Text + text: "${experienceYears} Yrs" + variant: body + - component: Column + align: center + children: + - component: Text + text: Tasks + variant: caption + - component: Text + text: "${completedTasks} Done" + variant: body + - component: Text + text: Verified Core Contributor + variant: caption + +sampleData: + userName: Alice Smith + role: Systems Architect + experienceYears: 9 + completedTasks: 142 diff --git a/samples/community/templates/templates/team_roster.yaml b/samples/community/templates/templates/team_roster.yaml new file mode 100644 index 0000000000..1abfb112a7 --- /dev/null +++ b/samples/community/templates/templates/team_roster.yaml @@ -0,0 +1,29 @@ +version: "0.1" +templateId: TeamRoster +description: Team directory roster with styled header banner and nested team lists. + +parameters: + directoryTitle: + type: string + title: Directory Title + description: Title text for the team directory. + children: + type: children + title: Team Cards List + description: List of child TeamCard components. + default: [] + +layout: + component: Column + children: + - component: Text + text: ${directoryTitle} + variant: h1 + - component: Divider + axis: horizontal + - component: Column + children: ${children} + +sampleData: + directoryTitle: Global Engineering Directory + children: [] diff --git a/samples/community/templates/templates/two_column_layout.yaml b/samples/community/templates/templates/two_column_layout.yaml new file mode 100644 index 0000000000..78115c7eaf --- /dev/null +++ b/samples/community/templates/templates/two_column_layout.yaml @@ -0,0 +1,40 @@ +version: "0.1" +templateId: TwoColumnLayout +description: Responsive two-column dashboard layout with banner header and split content regions. + +parameters: + headerChild: + type: child + title: Header Component Slot + description: Single child component ID mounted at the top banner. + leftChildren: + type: children + title: Left Column Children + description: Child components placed in the left primary region. + default: [] + rightChildren: + type: children + title: Right Column Children + description: Child components placed in the right secondary region. + default: [] + +layout: + component: Column + children: + - component: Column + children: ${headerChild} + - component: Divider + axis: horizontal + - component: Row + children: + - component: Column + children: ${leftChildren} + - component: Column + children: ${rightChildren} + +sampleData: + headerChild: + component: Text + text: Header Title + leftChildren: [] + rightChildren: [] diff --git a/samples/community/templates/templates/user_profile.yaml b/samples/community/templates/templates/user_profile.yaml new file mode 100644 index 0000000000..ec722b559e --- /dev/null +++ b/samples/community/templates/templates/user_profile.yaml @@ -0,0 +1,38 @@ +version: "0.1" +templateId: UserProfile +description: User profile card displaying avatar, full name, and role. + +parameters: + userId: + type: string + title: User ID + description: Unique user account ID. + userName: + type: string + title: User Name + description: Full name of the user. + role: + type: string + title: Role + description: Job title or role. + default: Member + +layout: + component: Card + child: + component: Column + align: center + children: + - component: Icon + name: person + - component: Text + text: ${userName} + variant: h3 + - component: Text + text: ${role} + variant: caption + +sampleData: + userId: usr_101 + userName: Alice Smith + role: Lead Architect diff --git a/samples/community/templates/test_e2e.mjs b/samples/community/templates/test_e2e.mjs new file mode 100644 index 0000000000..ec364cb3ab --- /dev/null +++ b/samples/community/templates/test_e2e.mjs @@ -0,0 +1,256 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import {chromium} from 'playwright'; + +async function runE2ETest() { + console.log( + '🚀 Starting A2UI Templates End-to-End Test (Presets + Inspector + Template Library + Live LLM)...', + ); + + const browser = await chromium.launch({headless: true}); + const page = await browser.newPage(); + + const pageErrors = []; + page.on('pageerror', err => { + console.error('❌ Page Error:', err.message); + pageErrors.push(err.message); + }); + + try { + // 1. Navigate to client + console.log('🌐 Navigating to http://localhost:5173...'); + await page.goto('http://localhost:5173', {waitUntil: 'networkidle', timeout: 15000}); + + const title = await page.textContent('h1'); + console.log(`✅ Loaded application: "${title?.trim()}"`); + + // Helper to verify a preset button + async function testPreset(buttonText, expectedContents) { + console.log(`\n👉 Clicking preset: "${buttonText}"...`); + const btn = page.locator(`button:has-text("${buttonText}")`); + await btn.click(); + + // Wait for assistant response and surface to render + await page.waitForTimeout(1000); + + const bodyText = await page.textContent('body'); + if ( + bodyText.includes('Error contacting server:') || + bodyText.includes('Validation failed') || + bodyText.includes('Catalog not found') + ) { + throw new Error(`Client reported error after clicking "${buttonText}":\n${bodyText}`); + } + + for (const expected of expectedContents) { + if (!bodyText.includes(expected)) { + throw new Error( + `Expected text "${expected}" not found in rendered DOM for preset "${buttonText}".`, + ); + } + console.log(` ✓ Found rendered text: "${expected}"`); + } + console.log(`✅ Preset "${buttonText}" passed!`); + } + + // 2. Test Presets + await testPreset('🔒 Verified Salary', [ + 'Verified Compensation', + 'Marcus Vance', + '$195,000', + '$38,000', + '2,800 RSUs', + ]); + await testPreset('💰 Payroll Summary', [ + 'Payroll & Compensation Summary', + 'TOTAL PAYROLL', + '$795,000', + 'Total Budget: $952,000', + ]); + await testPreset('👤 User Profile', ['Alice Smith', 'Lead Architect']); + await testPreset('👥 Team Roster', [ + 'Organization Directory', + 'Core Architecture', + 'Dr. Elena Vance', + 'Design Systems', + 'Aria Chen', + ]); + await testPreset('🎯 Team Goals', [ + 'Strategic Objectives: Core Protocol Engineering', + 'Deliver synchronous template expansion engine', + 'High', + ]); + await testPreset('💬 Feedback Board', [ + 'Feedback & Retrospective: Frontend & Protocols Guild', + 'Dr. Elena Vance', + 'Marcus Vance', + ]); + await testPreset('⭐ Competency Panel', [ + 'Competency: Alice Smith', + 'Lead Systems Architect', + '9 Yrs', + '142 Done', + ]); + + // 3. Test Inspector UI on Turn + console.log('\n👉 Testing Format & JSON Inspector Drawer...'); + const inspectBtn = page.locator('button:has-text("Inspect Format")').last(); + await inspectBtn.click(); + await page.waitForTimeout(500); + + // Verify Express DSL is visible + let inspectorContent = await page.textContent('body'); + if ( + !inspectorContent.includes('') && + !inspectorContent.includes('UserProfile') && + !inspectorContent.includes('EmployeeSalaryCard') + ) { + throw new Error(`Expected Express DSL content in inspector drawer:\n${inspectorContent}`); + } + console.log(' ✓ Raw Express DSL displayed in inspector drawer'); + + // Switch to Expanded JSON Tab + const jsonTabBtn = page.locator('button:has-text("Expanded A2UI JSON")'); + await jsonTabBtn.click(); + await page.waitForTimeout(500); + + inspectorContent = await page.textContent('body'); + if ( + !inspectorContent.includes('createSurface') || + !inspectorContent.includes('updateComponents') + ) { + throw new Error(`Expected expanded JSON messages in inspector drawer:\n${inspectorContent}`); + } + console.log(' ✓ Expanded A2UI JSON displayed in inspector drawer'); + console.log('✅ Inspector Drawer Test Passed!'); + + // 4. Test Template Library Screen + console.log('\n👉 Testing Template Library Studio...'); + const libraryTabBtn = page.locator('button:has-text("Template Library")'); + await libraryTabBtn.click(); + await page.waitForTimeout(1000); + + let libraryBody = await page.textContent('body'); + if ( + !libraryBody.includes('Registered Templates') || + (!libraryBody.includes('Inflated UI Preview') && + !libraryBody.includes('Inflated Output Preview')) + ) { + throw new Error(`Template Library studio failed to load:\n${libraryBody}`); + } + console.log(' ✓ Template Library screen mounted'); + + // Click EmployeeSalaryCard dynamic template in library list + console.log('\n👉 Testing Dynamic Template 3-Stage Studio...'); + const salaryCardBtn = page.locator('button:has-text("EmployeeSalaryCard")').first(); + await salaryCardBtn.click(); + await page.waitForTimeout(600); + + libraryBody = await page.textContent('body'); + if ( + !libraryBody.includes('Dynamic Server Resolver') || + !libraryBody.includes('Step 1: Simple LLM Input Interface') + ) { + throw new Error(`Dynamic template studio failed to mount:\n${libraryBody}`); + } + console.log(' ✓ Dynamic Template 3-Stage Studio mounted'); + + // Test dropdown selection to Marcus Vance + const empSelect = page.locator('select'); + await Promise.all([ + page.waitForResponse(resp => resp.url().includes('/resolve') && resp.status() === 200), + empSelect.selectOption('emp_102'), + ]); + await page.waitForTimeout(800); + + libraryBody = await page.textContent('body'); + if (!libraryBody.includes('Marcus Vance') || !libraryBody.includes('$195,000')) { + throw new Error(`Dynamic resolver execution for emp_102 failed:\n${libraryBody}`); + } + console.log(' ✓ Dynamic server resolver executed and updated preview for Marcus Vance'); + + // Test Stage 2: Static Blueprint Tab + const layoutTabBtn = page.locator('button:has-text("2. Static Blueprint")'); + await layoutTabBtn.click(); + await page.waitForTimeout(400); + + libraryBody = await page.textContent('body'); + if (!libraryBody.includes('salary_card.yaml') && !libraryBody.includes('baseSalary')) { + throw new Error(`Static Blueprint stage failed to display:\n${libraryBody}`); + } + console.log(' ✓ Static Blueprint stage displayed underlying salary_card.yaml layout'); + + // Test Stage 3: Resolved Output Tab + const resolvedTabBtn = page.locator('button:has-text("3. Resolved Output")'); + await resolvedTabBtn.click(); + await page.waitForTimeout(400); + + libraryBody = await page.textContent('body'); + if (!libraryBody.includes('$195,000') || !libraryBody.includes('$38,000')) { + throw new Error(`Resolved Output stage failed to display injected record:\n${libraryBody}`); + } + console.log(' ✓ Resolved Output stage displayed live injected database figures'); + console.log('✅ Dynamic Template 3-Stage Studio Passed!'); + + // Switch back to Interactive Chat + const chatTabBtn = page.locator('button:has-text("Interactive Chat")'); + await chatTabBtn.click(); + await page.waitForTimeout(500); + + // 5. Test Live LLM Request + console.log('\n👉 Testing Live Gemini LLM Generation...'); + const input = page.locator('input[type="text"]'); + await input.fill('Create a team goal list for Cloud Platform team with 2 goals'); + const sendBtn = page.locator('button:has-text("Send")'); + await sendBtn.click(); + + // Wait for LLM generation and client mounting + console.log(' Waiting for live Gemini inference & template expansion...'); + await page.waitForTimeout(6000); + + const liveBodyText = await page.textContent('body'); + if ( + liveBodyText.includes('Error contacting server:') || + liveBodyText.includes('Validation failed') || + liveBodyText.includes('Catalog not found') + ) { + throw new Error(`Live LLM request failed with error in client:\n${liveBodyText}`); + } + + if ( + !liveBodyText.includes('Cloud Platform') && + !liveBodyText.includes('Strategic Objectives') + ) { + throw new Error( + `Expected live generated goal card not found in rendered DOM:\n${liveBodyText}`, + ); + } + console.log(' ✓ Live LLM generated card rendered cleanly in DOM!'); + console.log('✅ Live LLM Request Passed!'); + + if (pageErrors.length > 0) { + throw new Error(`Encountered ${pageErrors.length} unhandled page errors during test run.`); + } + + console.log('\n🎉 ALL PRESETS, INSPECTOR, TEMPLATE LIBRARY, AND LIVE LLM TESTS PASSED! 🎉\n'); + } finally { + await browser.close(); + } +} + +runE2ETest().catch(err => { + console.error('\n❌ E2E TEST FAILED:', err); + process.exit(1); +}); diff --git a/samples/community/templates/uv.lock b/samples/community/templates/uv.lock new file mode 100644 index 0000000000..4c605565f3 --- /dev/null +++ b/samples/community/templates/uv.lock @@ -0,0 +1,2231 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", +] + +[[package]] +name = "a2a-sdk" +version = "0.3.26" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-api-core" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/a2a-sdk/a2a_sdk-0.3.26.tar.gz", hash = "sha256:44068e2d037afbb07ab899267439e9bc7eaa7ac2af94f1e8b239933c993ad52d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/a2a-sdk/a2a_sdk-0.3.26-py3-none-any.whl", hash = "sha256:754e0573f6d33b225c1d8d51f640efa69cbbed7bdfb06ce9c3540ea9f58d4a91" }, +] + +[[package]] +name = "a2ui-agent-sdk" +source = { editable = "../../../agent_sdks/python/a2ui_agent" } +dependencies = [ + { name = "a2a-sdk" }, + { name = "a2ui-core" }, + { name = "antlr4-python3-runtime" }, + { name = "google-adk" }, + { name = "google-genai" }, + { name = "jsonschema" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2a-sdk", specifier = ">=0.3.0,<0.4.0" }, + { name = "a2ui-core", editable = "../../../agent_sdks/python/a2ui_core" }, + { name = "antlr4-python3-runtime", specifier = ">=4.13.0,<4.14.0" }, + { name = "google-adk", specifier = ">=1.28.1" }, + { name = "google-genai", specifier = ">=1.27.0" }, + { name = "jsonschema", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "antlr4-tools", specifier = ">=0.2.1" }, + { name = "hatchling", specifier = ">=1.30.1" }, + { name = "types-jsonschema" }, + { name = "types-pyyaml" }, +] + +[[package]] +name = "a2ui-core" +source = { editable = "../../../agent_sdks/python/a2ui_core" } +dependencies = [ + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "referencing" }, +] + +[package.metadata] +requires-dist = [ + { name = "jsonschema", specifier = ">=4.26.0" }, + { name = "pydantic", specifier = ">=2.10.0" }, + { name = "referencing", specifier = ">=0.37.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pyyaml", specifier = ">=6.0.3" }] + +[[package]] +name = "a2ui-templates-demo-server" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "a2ui-agent-sdk" }, + { name = "a2ui-core" }, + { name = "fastapi" }, + { name = "google-genai" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "a2ui-agent-sdk", editable = "../../../agent_sdks/python/a2ui_agent" }, + { name = "a2ui-core", editable = "../../../agent_sdks/python/a2ui_core" }, + { name = "fastapi", specifier = ">=0.115.0" }, + { name = "google-genai", specifier = ">=1.0.0" }, + { name = "uvicorn", specifier = ">=0.30.0" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohappyeyeballs/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohappyeyeballs/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiosignal/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiosignal/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e" }, +] + +[[package]] +name = "aiosqlite" +version = "0.22.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiosqlite/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/aiosqlite/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/antlr4-python3-runtime/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/antlr4-python3-runtime/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/async-timeout/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/async-timeout/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309" }, +] + +[[package]] +name = "authlib" +version = "1.7.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "joserfc" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/click/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/click/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/cryptography/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/exceptiongroup/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/exceptiongroup/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d" }, +] + +[[package]] +name = "google-adk" +version = "2.7.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "aiohttp" }, + { name = "aiosqlite" }, + { name = "authlib" }, + { name = "click" }, + { name = "fastapi" }, + { name = "google-auth", extra = ["pyopenssl"] }, + { name = "google-genai" }, + { name = "graphviz" }, + { name = "httpx" }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "starlette" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "tzlocal" }, + { name = "uvicorn" }, + { name = "watchdog" }, + { name = "websockets" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-2.7.1.tar.gz", hash = "sha256:0485bfba1a04960a3784eb67531491475be1a3f8acefcc5c880a906857ecb3e2" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-2.7.1-py3-none-any.whl", hash = "sha256:cd21e37c9846a80086fd880924aa5548e625ef85178122b40cda81ef5316129f" }, +] + +[[package]] +name = "google-api-core" +version = "2.35.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-api-core/google_api_core-2.35.0-py3-none-any.whl", hash = "sha256:88ce7a11146e1ddd331f7d2fd379787eb7e9015c34c6ed681e4f5fb8af3615af" }, +] + +[[package]] +name = "google-auth" +version = "2.57.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-auth/google_auth-2.57.0-py3-none-any.whl", hash = "sha256:180dafe015cfb62193bea26b677500fab5b9fd51a1e825ebf3ad9b182047ae59" }, +] + +[package.optional-dependencies] +pyopenssl = [ + { name = "cryptography" }, +] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "2.19.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-2.19.0.tar.gz", hash = "sha256:d8f4126643793a7de230c396bcd142d21c948c8bb57507580e152549a7a41d9d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-2.19.0-py3-none-any.whl", hash = "sha256:36e0326dd886b52ef765be4c46042732b46b21f637abbe060e3db7c3de23974c" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpx/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpx/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpx-sse/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/httpx-sse/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4" }, +] + +[[package]] +name = "joserfc" +version = "1.7.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/joserfc/joserfc-1.7.4.tar.gz", hash = "sha256:b3bc561672ae541b17a9237053b48a03dacddd92d68047b3ecdfb4b5714a88ed" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/joserfc/joserfc-1.7.4-py3-none-any.whl", hash = "sha256:32d46c2cd5e3203c13e87a6c61333cab310b1ba80cd54b4c4f386a848a122463" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281" }, +] + +[[package]] +name = "protobuf" +version = "7.36.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0.tar.gz", hash = "sha256:e8e09cb0d794c6687926fa558a8a6e72aa10edb997d5ca61da0765f12a3e00ea" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:9103532dffd80c6fab7e50c65a31007680a06eb57537d437bb1b35812c138a37" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:bf94a5917c71058262de683669bc0a797a7669d3de71f0b36d058e3194f47b44" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:3297e60abdff301e5f74393d87f6cc59dacab5f024a89548a6e8de1d26576b16" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:70f5ec8eb0da81a44360c0dc0beac99a0d78071d21956a7076bae8bd2051841b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-win32.whl", hash = "sha256:7326fd717bdc419162a735938d89d4032332bcc3408804012b24ff3a37086071" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-cp310-abi3-win_amd64.whl", hash = "sha256:1781cc1de61249b750848029bca452c0a8b7e990080316b9bbc2518b2117b488" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-7.36.0-py3-none-any.whl", hash = "sha256:53374d53fc29a67f7dbbf0ade47d7526a0f0137bf0f9c90e48d8a60790ef748c" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyasn1-modules/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyasn1-modules/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py", version = "0.30.0", source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version < '3.11'" }, + { name = "rpds-py", version = "2026.6.3", source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" }, marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931" }, +] + +[[package]] +name = "tzlocal" +version = "5.4.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/simple/" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f" } +wheels = [ + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688" }, + { url = "http://airlock-proxy.uplink.goog:999/python/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7" }, +] diff --git a/samples/community/yarn.lock b/samples/community/yarn.lock index a5171ba747..4d98e596b0 100644 --- a/samples/community/yarn.lock +++ b/samples/community/yarn.lock @@ -129,6 +129,46 @@ __metadata: languageName: unknown linkType: soft +"@a2ui/react@npm:^0.10.2": + version: 0.10.2 + resolution: "@a2ui/react@npm:0.10.2" + dependencies: + "@a2ui/markdown-it": "npm:*" + "@a2ui/web_core": "npm:^0.10.5" + clsx: "npm:^2.1.1" + markdown-it: "npm:^14.2.0" + zod: "npm:^3.25.76" + peerDependencies: + react: ^19.2.7 + react-dom: ^19.2.7 + zod: ^3.25.76 + checksum: 10c0/61db572c7f57079052d452233f58cce68e9e35b72d7b004e702741304f866960d0045d976f354c15dfac223981b1a92550bbc6870ba1002df6ca9670ed45968e + languageName: node + linkType: hard + +"@a2ui/templates-community-demo@workspace:templates/client": + version: 0.0.0-use.local + resolution: "@a2ui/templates-community-demo@workspace:templates/client" + dependencies: + "@a2ui/react": "npm:^0.10.2" + "@a2ui/web_core": "npm:^0.10.6" + "@types/node": "npm:^25.9.3" + "@types/react": "npm:^19.2.17" + "@types/react-dom": "npm:^19.2.3" + "@vitejs/plugin-react": "npm:^6.0.2" + eslint: "npm:^10.4.1" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-react: "npm:^7.37.5" + eslint-plugin-react-hooks: "npm:^7.1.1" + react: "npm:^19.2.7" + react-dom: "npm:^19.2.7" + typescript: "npm:5.9.3" + typescript-eslint: "npm:^8.61.0" + vite: "npm:^8.0.16" + wireit: "npm:^0.15.0-pre.2" + languageName: unknown + linkType: soft + "@a2ui/web_core@npm:^0.10.0": version: 0.10.0 resolution: "@a2ui/web_core@npm:0.10.0" @@ -141,6 +181,18 @@ __metadata: languageName: node linkType: hard +"@a2ui/web_core@npm:^0.10.5, @a2ui/web_core@npm:^0.10.6": + version: 0.10.6 + resolution: "@a2ui/web_core@npm:0.10.6" + dependencies: + "@preact/signals-core": "npm:^1.14.2" + date-fns: "npm:^4.4.0" + zod: "npm:^3.25.76" + zod-to-json-schema: "npm:^3.25.2" + checksum: 10c0/481b90b4caa1428b3fa8344ab0ef5a515545e8fa6ef62e2fa6121968f80a2aab329e48be002e4590f770bd479872aa5963a5db00b12e75e063e2c7846234654b + languageName: node + linkType: hard + "@algolia/abtesting@npm:1.14.1": version: 1.14.1 resolution: "@algolia/abtesting@npm:1.14.1" @@ -842,6 +894,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.4": + version: 7.29.8 + resolution: "@babel/parser@npm:7.29.8" + dependencies: + "@babel/types": "npm:^7.29.8" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/acc890c5e6a6dd40863a47b50bac111d7185ee6fbbe163ebe11d5214854ca2adb901462ad4d718a65090ef84bd2230e9e8ab45a2e0caccc685f1f57ab0bb1e28 + languageName: node + linkType: hard + "@babel/template@npm:^7.28.6, @babel/template@npm:^7.29.7": version: 7.29.7 resolution: "@babel/template@npm:7.29.7" @@ -878,6 +941,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.29.8": + version: 7.29.8 + resolution: "@babel/types@npm:7.29.8" + dependencies: + "@babel/helper-string-parser": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + checksum: 10c0/be7c279f0abf2a086c633e21b49c7ca80275d05283cc5a268b67a708c9914bd0c944f1422b3eb3cb37682a2af5d560abf520ccf9b01b53ecbfe6b71fbc3fdde6 + languageName: node + linkType: hard + "@bramus/specificity@npm:^2.4.2": version: 2.4.2 resolution: "@bramus/specificity@npm:2.4.2" @@ -1231,6 +1304,17 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.9.1": + version: 4.10.1 + resolution: "@eslint-community/eslint-utils@npm:4.10.1" + dependencies: + eslint-visitor-keys: "npm:^3.4.3" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 10c0/b514586655698bc6b74db72a496c77e813c78b63e36a83429845ac7057dd77113b0b6c31e6e590d5baaeee2abae6ea22363a41209bcafa078d895ab83ce83011 + languageName: node + linkType: hard + "@eslint-community/regexpp@npm:^4.12.2": version: 4.12.2 resolution: "@eslint-community/regexpp@npm:4.12.2" @@ -3270,6 +3354,13 @@ __metadata: languageName: node linkType: hard +"@preact/signals-core@npm:^1.14.2": + version: 1.14.4 + resolution: "@preact/signals-core@npm:1.14.4" + checksum: 10c0/963622e5c1c98a6946ec1ad01e9901ebacf316ab0a7e3c56c6ebaefee5b2c8ea03c3bdb8ba74120612b5f8976f5dc94693134670e372dded79db9994045aadd1 + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -4249,7 +4340,7 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:^1.0.0": +"@rolldown/pluginutils@npm:^1.0.0, @rolldown/pluginutils@npm:^1.0.1": version: 1.0.1 resolution: "@rolldown/pluginutils@npm:1.0.1" checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd @@ -4796,6 +4887,24 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^19.2.3": + version: 19.2.4 + resolution: "@types/react-dom@npm:19.2.4" + peerDependencies: + "@types/react": ^19.2.0 + checksum: 10c0/b7d854ce17bb51a3a067168268a90f0123d10dc490f63f1ff5409f07ac715febeb8f5b7b473404a9d437106571e0312fc2937a945634d590abfc9aa46a14b01b + languageName: node + linkType: hard + +"@types/react@npm:^19.2.17": + version: 19.2.18 + resolution: "@types/react@npm:19.2.18" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10c0/d04216172b4b4362b310017c210dfbe019fb4f4e7dffd0313e70b3acb3051d5c3e76e7e84e3cf4c6a3c824993ffadfe3949e589a6398a09d4200e7670e2962de + languageName: node + linkType: hard + "@types/request@npm:^2.48.8": version: 2.48.13 resolution: "@types/request@npm:2.48.13" @@ -4857,6 +4966,141 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.67.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.67.0" + "@typescript-eslint/type-utils": "npm:8.67.0" + "@typescript-eslint/utils": "npm:8.67.0" + "@typescript-eslint/visitor-keys": "npm:8.67.0" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + "@typescript-eslint/parser": ^8.67.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/5962baaa764cd6350dfdf51c9a09fdd9ee6be65982ac562ee9b732f851744158e5006bf8ce61556a93534d831be8300fd89d7b79b6b4d7170dc2381d987dc8d9 + languageName: node + linkType: hard + +"@typescript-eslint/parser@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/parser@npm:8.67.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:8.67.0" + "@typescript-eslint/types": "npm:8.67.0" + "@typescript-eslint/typescript-estree": "npm:8.67.0" + "@typescript-eslint/visitor-keys": "npm:8.67.0" + debug: "npm:^4.4.3" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/8f6f8fbea429509ca0c95c4d48a45da0c890172a590606d65587e75a3ca762c3e5678a20a63fc9e386b0b21b7aa643ba9724354ceaa75bc8f62d0b22cb0198c8 + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/project-service@npm:8.67.0" + dependencies: + "@typescript-eslint/tsconfig-utils": "npm:^8.67.0" + "@typescript-eslint/types": "npm:^8.67.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/d8f89dc209f2186c04fb1c42c1a5c69c21696a7a57c5f2be2222682a6440b49ec32687ae85cbd1c1c164431635fbc37bb9404b336e5748966e270ee1347e028c + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/scope-manager@npm:8.67.0" + dependencies: + "@typescript-eslint/types": "npm:8.67.0" + "@typescript-eslint/visitor-keys": "npm:8.67.0" + checksum: 10c0/8f1fe7dffcb6929ad66dcdca77dd1e4a703f18ab3ab1d685458af74dad10b9be05795e64bd58ff60b45cda8d45737240c84874674d39140c2689e00e3ee82bb9 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.67.0, @typescript-eslint/tsconfig-utils@npm:^8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.67.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/c19161347ea3d7a0081653c4d399275a3e0d66f87a24131fb5a358e6b55bc27d709781dbda16382f3f721d790338dcf42c65c9e6c26077123e9f3d076d37a894 + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/type-utils@npm:8.67.0" + dependencies: + "@typescript-eslint/types": "npm:8.67.0" + "@typescript-eslint/typescript-estree": "npm:8.67.0" + "@typescript-eslint/utils": "npm:8.67.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/0970ea126f9b63a6eb9ca3525a6bf3065dffb3f61a00bd0ee84967140e7ecffe45cafda0cd2c90233dcc06b19c8fb98f4cd90ba8ebbb12deaf9501b9f5b54d8c + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:8.67.0, @typescript-eslint/types@npm:^8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/types@npm:8.67.0" + checksum: 10c0/b892a00d4cbea9604a0abf6eedd0ea019b27df4220a1d90e0035101cb4f846722c9e3eeadce40b423c1e0240709bbc787c6ca49bcc4f8c25ba23b4bd0436492a + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.67.0" + dependencies: + "@typescript-eslint/project-service": "npm:8.67.0" + "@typescript-eslint/tsconfig-utils": "npm:8.67.0" + "@typescript-eslint/types": "npm:8.67.0" + "@typescript-eslint/visitor-keys": "npm:8.67.0" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.5.0" + peerDependencies: + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/449350fcf4f4ee55a6bb7a73302c4eef072a1282d366344cb625c0f17231eb4088251e6ce61fb48d0d536febb9a28f3725b9bd78ac8d0dbf9c161cf51745870c + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/utils@npm:8.67.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.67.0" + "@typescript-eslint/types": "npm:8.67.0" + "@typescript-eslint/typescript-estree": "npm:8.67.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/cccaca1aeba52ec332ee95f3367b84eaf5dd9d60a0d1b4332ebe41b775fa7e8b8faf412de4d88b73a22f84b01f6ab48496c61863407b08fcbef9af429b6b89f1 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.67.0": + version: 8.67.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.67.0" + dependencies: + "@typescript-eslint/types": "npm:8.67.0" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10c0/d43e6151cd1cdbcfb2f9fa54946198baaebaf0b7a9057ccb2f1aeba1aa3eb46708a97cd7773dcfd0eba3bd45642a5520f4607001cd27d8958e41aa56b2fe7a35 + languageName: node + linkType: hard + "@vitejs/plugin-basic-ssl@npm:2.1.4": version: 2.1.4 resolution: "@vitejs/plugin-basic-ssl@npm:2.1.4" @@ -4866,6 +5110,24 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^6.0.2": + version: 6.0.5 + resolution: "@vitejs/plugin-react@npm:6.0.5" + dependencies: + "@rolldown/pluginutils": "npm:^1.0.1" + peerDependencies: + "@rolldown/plugin-babel": ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + "@rolldown/plugin-babel": + optional: true + babel-plugin-react-compiler: + optional: true + checksum: 10c0/fb02246fe3652d7fb746190bdd098b9e29918dfcf8c67b9c0c37300ce789e82331b2ba761530ac6ac9a61390b774d44f401787bd1c87a6c866d1e74a745cc1a0 + languageName: node + linkType: hard + "@vitest/expect@npm:4.1.9": version: 4.1.9 resolution: "@vitest/expect@npm:4.1.9" @@ -5301,6 +5563,98 @@ __metadata: languageName: node linkType: hard +"array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": + version: 1.0.2 + resolution: "array-buffer-byte-length@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.3" + is-array-buffer: "npm:^3.0.5" + checksum: 10c0/74e1d2d996941c7a1badda9cabb7caab8c449db9086407cad8a1b71d2604cc8abf105db8ca4e02c04579ec58b7be40279ddb09aea4784832984485499f48432d + languageName: node + linkType: hard + +"array-includes@npm:^3.1.6, array-includes@npm:^3.1.8": + version: 3.1.9 + resolution: "array-includes@npm:3.1.9" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.24.0" + es-object-atoms: "npm:^1.1.1" + get-intrinsic: "npm:^1.3.0" + is-string: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/0235fa69078abeac05ac4250699c44996bc6f774a9cbe45db48674ce6bd142f09b327d31482ff75cf03344db4ea03eae23edb862d59378b484b47ed842574856 + languageName: node + linkType: hard + +"array.prototype.findlast@npm:^1.2.5": + version: 1.2.5 + resolution: "array.prototype.findlast@npm:1.2.5" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10c0/ddc952b829145ab45411b9d6adcb51a8c17c76bf89c9dd64b52d5dffa65d033da8c076ed2e17091779e83bc892b9848188d7b4b33453c5565e65a92863cb2775 + languageName: node + linkType: hard + +"array.prototype.flat@npm:^1.3.1": + version: 1.3.3 + resolution: "array.prototype.flat@npm:1.3.3" + dependencies: + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10c0/d90e04dfbc43bb96b3d2248576753d1fb2298d2d972e29ca7ad5ec621f0d9e16ff8074dae647eac4f31f4fb7d3f561a7ac005fb01a71f51705a13b5af06a7d8a + languageName: node + linkType: hard + +"array.prototype.flatmap@npm:^1.3.3": + version: 1.3.3 + resolution: "array.prototype.flatmap@npm:1.3.3" + dependencies: + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10c0/ba899ea22b9dc9bf276e773e98ac84638ed5e0236de06f13d63a90b18ca9e0ec7c97d622d899796e3773930b946cd2413d098656c0c5d8cc58c6f25c21e6bd54 + languageName: node + linkType: hard + +"array.prototype.tosorted@npm:^1.1.4": + version: 1.1.4 + resolution: "array.prototype.tosorted@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.3" + es-errors: "npm:^1.3.0" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10c0/eb3c4c4fc0381b0bf6dba2ea4d48d367c2827a0d4236a5718d97caaccc6b78f11f4cadf090736e86301d295a6aa4967ed45568f92ced51be8cbbacd9ca410943 + languageName: node + linkType: hard + +"arraybuffer.prototype.slice@npm:^1.0.4": + version: 1.0.4 + resolution: "arraybuffer.prototype.slice@npm:1.0.4" + dependencies: + array-buffer-byte-length: "npm:^1.0.1" + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + is-array-buffer: "npm:^3.0.4" + checksum: 10c0/2f2459caa06ae0f7f615003f9104b01f6435cc803e11bd2a655107d52a1781dc040532dc44d93026b694cc18793993246237423e13a5337e86b43ed604932c06 + languageName: node + linkType: hard + "arrify@npm:^2.0.0": version: 2.0.1 resolution: "arrify@npm:2.0.1" @@ -5345,6 +5699,15 @@ __metadata: languageName: node linkType: hard +"available-typed-arrays@npm:^1.0.7": + version: 1.0.7 + resolution: "available-typed-arrays@npm:1.0.7" + dependencies: + possible-typed-array-names: "npm:^1.0.0" + checksum: 10c0/d07226ef4f87daa01bd0fe80f8f310982e345f372926da2e5296aecc25c41cab440916bbaa4c5e1034b453af3392f67df5961124e4b586df1e99793a1374bdb2 + languageName: node + linkType: hard + "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" @@ -5616,7 +5979,19 @@ __metadata: languageName: node linkType: hard -"call-bound@npm:^1.0.2": +"call-bind@npm:^1.0.7, call-bind@npm:^1.0.8, call-bind@npm:^1.0.9": + version: 1.0.9 + resolution: "call-bind@npm:1.0.9" + dependencies: + call-bind-apply-helpers: "npm:^1.0.2" + es-define-property: "npm:^1.0.1" + get-intrinsic: "npm:^1.3.0" + set-function-length: "npm:^1.2.2" + checksum: 10c0/a6621f6da1444481919ce3b4983dff725691e0754d3507ae483ce56e54985f2da7d6f1df512c56dbf28660745cf1ca52553f1fc9aef5557f3ce353ef14fab714 + languageName: node + linkType: hard + +"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": version: 1.0.4 resolution: "call-bound@npm:1.0.4" dependencies: @@ -6043,6 +6418,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce + languageName: node + linkType: hard + "custom-lit-components@workspace:custom-lit-components": version: 0.0.0-use.local resolution: "custom-lit-components@workspace:custom-lit-components" @@ -6074,7 +6456,40 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^4.1.0": +"data-view-buffer@npm:^1.0.2": + version: 1.0.2 + resolution: "data-view-buffer@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.3" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.2" + checksum: 10c0/7986d40fc7979e9e6241f85db8d17060dd9a71bd53c894fa29d126061715e322a4cd47a00b0b8c710394854183d4120462b980b8554012acc1c0fa49df7ad38c + languageName: node + linkType: hard + +"data-view-byte-length@npm:^1.0.2": + version: 1.0.2 + resolution: "data-view-byte-length@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.3" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.2" + checksum: 10c0/f8a4534b5c69384d95ac18137d381f18a5cfae1f0fc1df0ef6feef51ef0d568606d970b69e02ea186c6c0f0eac77fe4e6ad96fec2569cc86c3afcc7475068c55 + languageName: node + linkType: hard + +"data-view-byte-offset@npm:^1.0.1": + version: 1.0.1 + resolution: "data-view-byte-offset@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + is-data-view: "npm:^1.0.1" + checksum: 10c0/fa7aa40078025b7810dcffc16df02c480573b7b53ef1205aa6a61533011005c1890e5ba17018c692ce7c900212b547262d33279fde801ad9843edc0863bf78c4 + languageName: node + linkType: hard + +"date-fns@npm:^4.1.0, date-fns@npm:^4.4.0": version: 4.4.0 resolution: "date-fns@npm:4.4.0" checksum: 10c0/988f0a13db183f5dfc85c36bbb6847a9c135a9225888bbea4005876ec15539a8613c21a07370a4e7ea543918d5a1cafb423c528b42cdbbde5fdfddb178126b21 @@ -6124,6 +6539,17 @@ __metadata: languageName: node linkType: hard +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" + dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.0.1" + checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 + languageName: node + linkType: hard + "define-lazy-prop@npm:^3.0.0": version: 3.0.0 resolution: "define-lazy-prop@npm:3.0.0" @@ -6131,6 +6557,17 @@ __metadata: languageName: node linkType: hard +"define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": + version: 1.2.1 + resolution: "define-properties@npm:1.2.1" + dependencies: + define-data-property: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.0" + object-keys: "npm:^1.1.1" + checksum: 10c0/88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -6173,6 +6610,15 @@ __metadata: languageName: node linkType: hard +"doctrine@npm:^2.1.0": + version: 2.1.0 + resolution: "doctrine@npm:2.1.0" + dependencies: + esutils: "npm:^2.0.2" + checksum: 10c0/b6416aaff1f380bf56c3b552f31fdf7a69b45689368deca72d28636f41c16bb28ec3ebc40ace97db4c1afc0ceeb8120e8492fe0046841c94c2933b2e30a7d5ac + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -6230,7 +6676,7 @@ __metadata: languageName: node linkType: hard -"dunder-proto@npm:^1.0.1": +"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" dependencies: @@ -6380,7 +6826,81 @@ __metadata: languageName: node linkType: hard -"es-define-property@npm:^1.0.1": +"es-abstract-get@npm:^1.0.0": + version: 1.0.0 + resolution: "es-abstract-get@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.2" + is-callable: "npm:^1.2.7" + object-inspect: "npm:^1.13.4" + checksum: 10c0/f9b4838ae719752207383a6d95a74590f891122bf26b92f5e72eeedbe53771029e4561f1cf75ea19330b71bcf3d4f536fb0c8f7e2b601fe24d284f46e488c7e3 + languageName: node + linkType: hard + +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0, es-abstract@npm:^1.24.2": + version: 1.24.2 + resolution: "es-abstract@npm:1.24.2" + dependencies: + array-buffer-byte-length: "npm:^1.0.2" + arraybuffer.prototype.slice: "npm:^1.0.4" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + data-view-buffer: "npm:^1.0.2" + data-view-byte-length: "npm:^1.0.2" + data-view-byte-offset: "npm:^1.0.1" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + es-set-tostringtag: "npm:^2.1.0" + es-to-primitive: "npm:^1.3.0" + function.prototype.name: "npm:^1.1.8" + get-intrinsic: "npm:^1.3.0" + get-proto: "npm:^1.0.1" + get-symbol-description: "npm:^1.1.0" + globalthis: "npm:^1.0.4" + gopd: "npm:^1.2.0" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + internal-slot: "npm:^1.1.0" + is-array-buffer: "npm:^3.0.5" + is-callable: "npm:^1.2.7" + is-data-view: "npm:^1.0.2" + is-negative-zero: "npm:^2.0.3" + is-regex: "npm:^1.2.1" + is-set: "npm:^2.0.3" + is-shared-array-buffer: "npm:^1.0.4" + is-string: "npm:^1.1.1" + is-typed-array: "npm:^1.1.15" + is-weakref: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + object-inspect: "npm:^1.13.4" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.7" + own-keys: "npm:^1.0.1" + regexp.prototype.flags: "npm:^1.5.4" + safe-array-concat: "npm:^1.1.3" + safe-push-apply: "npm:^1.0.0" + safe-regex-test: "npm:^1.1.0" + set-proto: "npm:^1.0.0" + stop-iteration-iterator: "npm:^1.1.0" + string.prototype.trim: "npm:^1.2.10" + string.prototype.trimend: "npm:^1.0.9" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.3" + typed-array-byte-length: "npm:^1.0.3" + typed-array-byte-offset: "npm:^1.0.4" + typed-array-length: "npm:^1.0.7" + unbox-primitive: "npm:^1.1.0" + which-typed-array: "npm:^1.1.19" + checksum: 10c0/67a5bf21ef5c7d775e6f6131a836323900b4d87194cf544394ac68fe31c57fa53828b978af4a4f551ef307f83a2f910a16b6b982760ad3ddc3dc471f98d5fd1b + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.0, es-define-property@npm:^1.0.1": version: 1.0.1 resolution: "es-define-property@npm:1.0.1" checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c @@ -6394,6 +6914,30 @@ __metadata: languageName: node linkType: hard +"es-iterator-helpers@npm:^1.2.1": + version: 1.4.0 + resolution: "es-iterator-helpers@npm:1.4.0" + dependencies: + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.24.2" + es-errors: "npm:^1.3.0" + es-set-tostringtag: "npm:^2.1.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.3.0" + globalthis: "npm:^1.0.4" + gopd: "npm:^1.2.0" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + internal-slot: "npm:^1.1.0" + iterator.prototype: "npm:^1.1.5" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/839a5e881446e1b4ab270a9ad5d01a23b83a38fadb9e526674df171357e90ad3111dc8c0b15e7d30db1958f2c3332dd963fab7d55f406a660d098b2b7eefb218 + languageName: node + linkType: hard + "es-module-lexer@npm:^2.0.0": version: 2.1.0 resolution: "es-module-lexer@npm:2.1.0" @@ -6401,7 +6945,7 @@ __metadata: languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1, es-object-atoms@npm:^1.1.2": version: 1.1.2 resolution: "es-object-atoms@npm:1.1.2" dependencies: @@ -6422,6 +6966,29 @@ __metadata: languageName: node linkType: hard +"es-shim-unscopables@npm:^1.0.2": + version: 1.1.0 + resolution: "es-shim-unscopables@npm:1.1.0" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10c0/1b9702c8a1823fc3ef39035a4e958802cf294dd21e917397c561d0b3e195f383b978359816b1732d02b255ccf63e1e4815da0065b95db8d7c992037be3bbbcdb + languageName: node + linkType: hard + +"es-to-primitive@npm:^1.3.0": + version: 1.3.4 + resolution: "es-to-primitive@npm:1.3.4" + dependencies: + es-abstract-get: "npm:^1.0.0" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + is-callable: "npm:^1.2.7" + is-date-object: "npm:^1.1.0" + is-symbol: "npm:^1.1.1" + checksum: 10c0/b10029f8b0b13841bade224ff39005a13d76d9cb803cd1efb18cc96a24414e83e2e7ed15d7ad9d0d0bda66884afd211b7b579b8fa31940e05b060934aa7077d8 + languageName: node + linkType: hard + "es-toolkit@npm:^1.39.7": version: 1.47.1 resolution: "es-toolkit@npm:1.47.1" @@ -6546,8 +7113,62 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^9.1.2": - version: 9.1.2 +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10c0/e1bcfadc9eccd526c240056b1e59c5cd26544fe59feb85f38f4f1f116caed96aea0b3b87868e68b3099e55caaac3f2e5b9f58110f85db893e83a332751192682 + languageName: node + linkType: hard + +"eslint-plugin-react-hooks@npm:^7.1.1": + version: 7.1.1 + resolution: "eslint-plugin-react-hooks@npm:7.1.1" + dependencies: + "@babel/core": "npm:^7.24.4" + "@babel/parser": "npm:^7.24.4" + hermes-parser: "npm:^0.25.1" + zod: "npm:^3.25.0 || ^4.0.0" + zod-validation-error: "npm:^3.5.0 || ^4.0.0" + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/cee8454915d71ac5d70a0d8f4f260e76eaf45fcd4162747dd4282b792ee5616d187351dabe6cdcff9040c79d0cec625635c4fd0777276be119efa88ebe058525 + languageName: node + linkType: hard + +"eslint-plugin-react@npm:^7.37.5": + version: 7.37.5 + resolution: "eslint-plugin-react@npm:7.37.5" + dependencies: + array-includes: "npm:^3.1.8" + array.prototype.findlast: "npm:^1.2.5" + array.prototype.flatmap: "npm:^1.3.3" + array.prototype.tosorted: "npm:^1.1.4" + doctrine: "npm:^2.1.0" + es-iterator-helpers: "npm:^1.2.1" + estraverse: "npm:^5.3.0" + hasown: "npm:^2.0.2" + jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" + minimatch: "npm:^3.1.2" + object.entries: "npm:^1.1.9" + object.fromentries: "npm:^2.0.8" + object.values: "npm:^1.2.1" + prop-types: "npm:^15.8.1" + resolve: "npm:^2.0.0-next.5" + semver: "npm:^6.3.1" + string.prototype.matchall: "npm:^4.0.12" + string.prototype.repeat: "npm:^1.0.0" + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + checksum: 10c0/c850bfd556291d4d9234f5ca38db1436924a1013627c8ab1853f77cac73ec19b020e861e6c7b783436a48b6ffcdfba4547598235a37ad4611b6739f65fd8ad57 + languageName: node + linkType: hard + +"eslint-scope@npm:^9.1.2": + version: 9.1.2 resolution: "eslint-scope@npm:9.1.2" dependencies: "@types/esrecurse": "npm:^4.3.1" @@ -6565,7 +7186,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^5.0.1": +"eslint-visitor-keys@npm:^5.0.0, eslint-visitor-keys@npm:^5.0.1": version: 5.0.1 resolution: "eslint-visitor-keys@npm:5.0.1" checksum: 10c0/16190bdf2cbae40a1109384c94450c526a79b0b9c3cb21e544256ed85ac48a4b84db66b74a6561d20fe6ab77447f150d711c2ad5ad74df4fcc133736bce99678 @@ -6646,7 +7267,7 @@ __metadata: languageName: node linkType: hard -"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0": +"estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" checksum: 10c0/1ff9447b96263dec95d6d67431c5e0771eb9776427421260a3e2f0fdd5d6bd4f8e37a7338f5ad2880c9f143450c9b1e4fc2069060724570a49cf9cf0312bd107 @@ -7030,6 +7651,15 @@ __metadata: languageName: node linkType: hard +"for-each@npm:^0.3.3, for-each@npm:^0.3.5": + version: 0.3.5 + resolution: "for-each@npm:0.3.5" + dependencies: + is-callable: "npm:^1.2.7" + checksum: 10c0/0e0b50f6a843a282637d43674d1fb278dda1dd85f4f99b640024cfb10b85058aac0cc781bf689d5fe50b4b7f638e91e548560723a4e76e04fe96ae35ef039cee + languageName: node + linkType: hard + "foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" @@ -7119,6 +7749,23 @@ __metadata: languageName: node linkType: hard +"function.prototype.name@npm:^1.1.6, function.prototype.name@npm:^1.1.8": + version: 1.2.0 + resolution: "function.prototype.name@npm:1.2.0" + dependencies: + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + functions-have-names: "npm:^1.2.3" + has-property-descriptors: "npm:^1.0.2" + hasown: "npm:^2.0.4" + is-callable: "npm:^1.2.7" + is-document.all: "npm:^1.0.0" + checksum: 10c0/b20e6370ef4f7d56d0bedf5719f6684a517a8dd3334209b4d9f51e8834859302a584187156bf024cda9f50ba2479e4d6764ac34af9532ea47d2f4d9fa6bcf90d + languageName: node + linkType: hard + "functional-red-black-tree@npm:^1.0.1": version: 1.0.1 resolution: "functional-red-black-tree@npm:1.0.1" @@ -7126,6 +7773,13 @@ __metadata: languageName: node linkType: hard +"functions-have-names@npm:^1.2.3": + version: 1.2.3 + resolution: "functions-have-names@npm:1.2.3" + checksum: 10c0/33e77fd29bddc2d9bb78ab3eb854c165909201f88c75faa8272e35899e2d35a8a642a15e7420ef945e1f64a9670d6aa3ec744106b2aa42be68ca5114025954ca + languageName: node + linkType: hard + "gaxios@npm:7.1.3": version: 7.1.3 resolution: "gaxios@npm:7.1.3" @@ -7223,7 +7877,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": version: 1.3.1 resolution: "get-intrinsic@npm:1.3.1" dependencies: @@ -7251,7 +7905,7 @@ __metadata: languageName: node linkType: hard -"get-proto@npm:^1.0.1": +"get-proto@npm:^1.0.0, get-proto@npm:^1.0.1": version: 1.0.1 resolution: "get-proto@npm:1.0.1" dependencies: @@ -7261,6 +7915,17 @@ __metadata: languageName: node linkType: hard +"get-symbol-description@npm:^1.1.0": + version: 1.1.0 + resolution: "get-symbol-description@npm:1.1.0" + dependencies: + call-bound: "npm:^1.0.3" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.6" + checksum: 10c0/d6a7d6afca375779a4b307738c9e80dbf7afc0bdbe5948768d54ab9653c865523d8920e670991a925936eb524b7cb6a6361d199a760b21d0ca7620194455aa4b + languageName: node + linkType: hard + "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -7327,6 +7992,16 @@ __metadata: languageName: node linkType: hard +"globalthis@npm:^1.0.4": + version: 1.0.4 + resolution: "globalthis@npm:1.0.4" + dependencies: + define-properties: "npm:^1.2.1" + gopd: "npm:^1.0.1" + checksum: 10c0/9d156f313af79d80b1566b93e19285f481c591ad6d0d319b4be5e03750d004dde40a39a0f26f7e635f9007a3600802f53ecd85a759b86f109e80a5f705e01846 + languageName: node + linkType: hard + "google-auth-library@npm:10.5.0": version: 10.5.0 resolution: "google-auth-library@npm:10.5.0" @@ -7410,7 +8085,7 @@ __metadata: languageName: node linkType: hard -"gopd@npm:^1.2.0": +"gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead @@ -7444,6 +8119,13 @@ __metadata: languageName: node linkType: hard +"has-bigints@npm:^1.0.2": + version: 1.1.0 + resolution: "has-bigints@npm:1.1.0" + checksum: 10c0/2de0cdc4a1ccf7a1e75ffede1876994525ac03cc6f5ae7392d3415dd475cd9eee5bceec63669ab61aa997ff6cceebb50ef75561c7002bed8988de2b9d1b40788 + languageName: node + linkType: hard + "has-flag@npm:^4.0.0": version: 4.0.0 resolution: "has-flag@npm:4.0.0" @@ -7451,6 +8133,24 @@ __metadata: languageName: node linkType: hard +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" + dependencies: + es-define-property: "npm:^1.0.0" + checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 + languageName: node + linkType: hard + +"has-proto@npm:^1.2.0": + version: 1.2.0 + resolution: "has-proto@npm:1.2.0" + dependencies: + dunder-proto: "npm:^1.0.0" + checksum: 10c0/46538dddab297ec2f43923c3d35237df45d8c55a6fc1067031e04c13ed8a9a8f94954460632fd4da84c31a1721eefee16d901cbb1ae9602bab93bb6e08f93b95 + languageName: node + linkType: hard + "has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": version: 1.1.0 resolution: "has-symbols@npm:1.1.0" @@ -7476,6 +8176,22 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10c0/48be3b2fa37a0cbc77a112a89096fa212f25d06de92781b163d67853d210a8a5c3784fac23d7d48335058f7ed283115c87b4332c2a2abaaccc76d0ead1a282ac + languageName: node + linkType: hard + +"hermes-parser@npm:^0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10c0/3abaa4c6f1bcc25273f267297a89a4904963ea29af19b8e4f6eabe04f1c2c7e9abd7bfc4730ddb1d58f2ea04b6fee74053d8bddb5656ec6ebf6c79cc8d14202c + languageName: node + linkType: hard + "hono@npm:^4.11.4": version: 4.12.25 resolution: "hono@npm:4.12.25" @@ -7629,6 +8345,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^7.0.5": + version: 7.0.6 + resolution: "ignore@npm:7.0.6" + checksum: 10c0/fc01ef1d14efbe003439b60538726351e81483d1b6f55bdbb3a4465c6346d9481afad5b350dfbd604ddd7049618ef9093ff26dc147984aabc303c56ba53ea3b5 + languageName: node + linkType: hard + "image-size@npm:~0.5.0": version: 0.5.5 resolution: "image-size@npm:0.5.5" @@ -7685,6 +8408,17 @@ __metadata: languageName: node linkType: hard +"internal-slot@npm:^1.1.0": + version: 1.1.0 + resolution: "internal-slot@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + hasown: "npm:^2.0.2" + side-channel: "npm:^1.1.0" + checksum: 10c0/03966f5e259b009a9bf1a78d60da920df198af4318ec004f57b8aef1dd3fe377fbc8cce63a96e8c810010302654de89f9e19de1cd8ad0061d15be28a695465c7 + languageName: node + linkType: hard + "interpret@npm:^1.0.0": version: 1.4.0 resolution: "interpret@npm:1.4.0" @@ -7706,6 +8440,39 @@ __metadata: languageName: node linkType: hard +"is-array-buffer@npm:^3.0.4, is-array-buffer@npm:^3.0.5": + version: 3.0.5 + resolution: "is-array-buffer@npm:3.0.5" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + get-intrinsic: "npm:^1.2.6" + checksum: 10c0/c5c9f25606e86dbb12e756694afbbff64bc8b348d1bc989324c037e1068695131930199d6ad381952715dad3a9569333817f0b1a72ce5af7f883ce802e49c83d + languageName: node + linkType: hard + +"is-async-function@npm:^2.0.0": + version: 2.1.1 + resolution: "is-async-function@npm:2.1.1" + dependencies: + async-function: "npm:^1.0.0" + call-bound: "npm:^1.0.3" + get-proto: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + safe-regex-test: "npm:^1.1.0" + checksum: 10c0/d70c236a5e82de6fc4d44368ffd0c2fee2b088b893511ce21e679da275a5ecc6015ff59a7d7e1bdd7ca39f71a8dbdd253cf8cce5c6b3c91cdd5b42b5ce677298 + languageName: node + linkType: hard + +"is-bigint@npm:^1.1.0": + version: 1.1.0 + resolution: "is-bigint@npm:1.1.0" + dependencies: + has-bigints: "npm:^1.0.2" + checksum: 10c0/f4f4b905ceb195be90a6ea7f34323bf1c18e3793f18922e3e9a73c684c29eeeeff5175605c3a3a74cc38185fe27758f07efba3dbae812e5c5afbc0d2316b40e4 + languageName: node + linkType: hard + "is-binary-path@npm:~2.1.0": version: 2.1.0 resolution: "is-binary-path@npm:2.1.0" @@ -7715,7 +8482,24 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.16.1": +"is-boolean-object@npm:^1.2.1": + version: 1.2.2 + resolution: "is-boolean-object@npm:1.2.2" + dependencies: + call-bound: "npm:^1.0.3" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/36ff6baf6bd18b3130186990026f5a95c709345c39cd368468e6c1b6ab52201e9fd26d8e1f4c066357b4938b0f0401e1a5000e08257787c1a02f3a719457001e + languageName: node + linkType: hard + +"is-callable@npm:^1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: 10c0/ceebaeb9d92e8adee604076971dd6000d38d6afc40bb843ea8e45c5579b57671c3f3b50d7f04869618242c6cee08d1b67806a8cb8edaaaf7c0748b3720d6066f + languageName: node + linkType: hard + +"is-core-module@npm:^2.16.1, is-core-module@npm:^2.16.2": version: 2.16.2 resolution: "is-core-module@npm:2.16.2" dependencies: @@ -7724,6 +8508,27 @@ __metadata: languageName: node linkType: hard +"is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2": + version: 1.0.2 + resolution: "is-data-view@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + get-intrinsic: "npm:^1.2.6" + is-typed-array: "npm:^1.1.13" + checksum: 10c0/ef3548a99d7e7f1370ce21006baca6d40c73e9f15c941f89f0049c79714c873d03b02dae1c64b3f861f55163ecc16da06506c5b8a1d4f16650b3d9351c380153 + languageName: node + linkType: hard + +"is-date-object@npm:^1.1.0": + version: 1.1.0 + resolution: "is-date-object@npm:1.1.0" + dependencies: + call-bound: "npm:^1.0.2" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/1a4d199c8e9e9cac5128d32e6626fa7805175af9df015620ac0d5d45854ccf348ba494679d872d37301032e35a54fc7978fba1687e8721b2139aea7870cafa2f + languageName: node + linkType: hard + "is-docker@npm:^3.0.0": version: 3.0.0 resolution: "is-docker@npm:3.0.0" @@ -7733,6 +8538,15 @@ __metadata: languageName: node linkType: hard +"is-document.all@npm:^1.0.0": + version: 1.0.0 + resolution: "is-document.all@npm:1.0.0" + dependencies: + call-bound: "npm:^1.0.4" + checksum: 10c0/955c20ed5bf01d49da8243b4c714947a6ff64b6d9ba0e12bdbfa654a3e7c47f72cc01c6cd2905e85512d02bc3a1290edd73857bca8842566ff9dcfb7c3f92dae + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -7740,6 +8554,15 @@ __metadata: languageName: node linkType: hard +"is-finalizationregistry@npm:^1.1.0": + version: 1.1.1 + resolution: "is-finalizationregistry@npm:1.1.1" + dependencies: + call-bound: "npm:^1.0.3" + checksum: 10c0/818dff679b64f19e228a8205a1e2d09989a98e98def3a817f889208cfcbf918d321b251aadf2c05918194803ebd2eb01b14fc9d0b2bea53d984f4137bfca5e97 + languageName: node + linkType: hard + "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -7756,6 +8579,19 @@ __metadata: languageName: node linkType: hard +"is-generator-function@npm:^1.0.10": + version: 1.1.2 + resolution: "is-generator-function@npm:1.1.2" + dependencies: + call-bound: "npm:^1.0.4" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + safe-regex-test: "npm:^1.1.0" + checksum: 10c0/83da102e89c3e3b71d67b51d47c9f9bc862bceb58f87201727e27f7fa19d1d90b0ab223644ecaee6fc6e3d2d622bb25c966fbdaf87c59158b01ce7c0fe2fa372 + languageName: node + linkType: hard + "is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" @@ -7783,6 +8619,30 @@ __metadata: languageName: node linkType: hard +"is-map@npm:^2.0.3": + version: 2.0.3 + resolution: "is-map@npm:2.0.3" + checksum: 10c0/2c4d431b74e00fdda7162cd8e4b763d6f6f217edf97d4f8538b94b8702b150610e2c64961340015fe8df5b1fcee33ccd2e9b62619c4a8a3a155f8de6d6d355fc + languageName: node + linkType: hard + +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: 10c0/bcdcf6b8b9714063ffcfa9929c575ac69bfdabb8f4574ff557dfc086df2836cf07e3906f5bbc4f2a5c12f8f3ba56af640c843cdfc74da8caed86c7c7d66fd08e + languageName: node + linkType: hard + +"is-number-object@npm:^1.1.1": + version: 1.1.1 + resolution: "is-number-object@npm:1.1.1" + dependencies: + call-bound: "npm:^1.0.3" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/97b451b41f25135ff021d85c436ff0100d84a039bb87ffd799cbcdbea81ef30c464ced38258cdd34f080be08fc3b076ca1f472086286d2aa43521d6ec6a79f53 + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -7804,6 +8664,34 @@ __metadata: languageName: node linkType: hard +"is-regex@npm:^1.2.1": + version: 1.2.1 + resolution: "is-regex@npm:1.2.1" + dependencies: + call-bound: "npm:^1.0.2" + gopd: "npm:^1.2.0" + has-tostringtag: "npm:^1.0.2" + hasown: "npm:^2.0.2" + checksum: 10c0/1d3715d2b7889932349241680032e85d0b492cfcb045acb75ffc2c3085e8d561184f1f7e84b6f8321935b4aea39bc9c6ba74ed595b57ce4881a51dfdbc214e04 + languageName: node + linkType: hard + +"is-set@npm:^2.0.3": + version: 2.0.3 + resolution: "is-set@npm:2.0.3" + checksum: 10c0/f73732e13f099b2dc879c2a12341cfc22ccaca8dd504e6edae26484bd5707a35d503fba5b4daad530a9b088ced1ae6c9d8200fd92e09b428fe14ea79ce8080b7 + languageName: node + linkType: hard + +"is-shared-array-buffer@npm:^1.0.4": + version: 1.0.4 + resolution: "is-shared-array-buffer@npm:1.0.4" + dependencies: + call-bound: "npm:^1.0.3" + checksum: 10c0/65158c2feb41ff1edd6bbd6fd8403a69861cf273ff36077982b5d4d68e1d59278c71691216a4a64632bd76d4792d4d1d2553901b6666d84ade13bba5ea7bc7db + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -7811,6 +8699,36 @@ __metadata: languageName: node linkType: hard +"is-string@npm:^1.1.1": + version: 1.1.1 + resolution: "is-string@npm:1.1.1" + dependencies: + call-bound: "npm:^1.0.3" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/2f518b4e47886bb81567faba6ffd0d8a8333cf84336e2e78bf160693972e32ad00fe84b0926491cc598dee576fdc55642c92e62d0cbe96bf36f643b6f956f94d + languageName: node + linkType: hard + +"is-symbol@npm:^1.1.1": + version: 1.1.1 + resolution: "is-symbol@npm:1.1.1" + dependencies: + call-bound: "npm:^1.0.2" + has-symbols: "npm:^1.1.0" + safe-regex-test: "npm:^1.1.0" + checksum: 10c0/f08f3e255c12442e833f75a9e2b84b2d4882fdfd920513cf2a4a2324f0a5b076c8fd913778e3ea5d258d5183e9d92c0cd20e04b03ab3df05316b049b2670af1e + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": + version: 1.1.15 + resolution: "is-typed-array@npm:1.1.15" + dependencies: + which-typed-array: "npm:^1.1.16" + checksum: 10c0/415511da3669e36e002820584e264997ffe277ff136643a3126cc949197e6ca3334d0f12d084e83b1994af2e9c8141275c741cf2b7da5a2ff62dd0cac26f76c4 + languageName: node + linkType: hard + "is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": version: 2.1.0 resolution: "is-unicode-supported@npm:2.1.0" @@ -7825,6 +8743,32 @@ __metadata: languageName: node linkType: hard +"is-weakmap@npm:^2.0.2": + version: 2.0.2 + resolution: "is-weakmap@npm:2.0.2" + checksum: 10c0/443c35bb86d5e6cc5929cd9c75a4024bb0fff9586ed50b092f94e700b89c43a33b186b76dbc6d54f3d3d09ece689ab38dcdc1af6a482cbe79c0f2da0a17f1299 + languageName: node + linkType: hard + +"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.1": + version: 1.1.1 + resolution: "is-weakref@npm:1.1.1" + dependencies: + call-bound: "npm:^1.0.3" + checksum: 10c0/8e0a9c07b0c780949a100e2cab2b5560a48ecd4c61726923c1a9b77b6ab0aa0046c9e7fb2206042296817045376dee2c8ab1dabe08c7c3dfbf195b01275a085b + languageName: node + linkType: hard + +"is-weakset@npm:^2.0.3": + version: 2.0.4 + resolution: "is-weakset@npm:2.0.4" + dependencies: + call-bound: "npm:^1.0.3" + get-intrinsic: "npm:^1.2.6" + checksum: 10c0/6491eba08acb8dc9532da23cb226b7d0192ede0b88f16199e592e4769db0a077119c1f5d2283d1e0d16d739115f70046e887e477eb0e66cd90e1bb29f28ba647 + languageName: node + linkType: hard + "is-what@npm:^4.1.8": version: 4.1.16 resolution: "is-what@npm:4.1.16" @@ -7841,6 +8785,13 @@ __metadata: languageName: node linkType: hard +"isarray@npm:^2.0.5": + version: 2.0.5 + resolution: "isarray@npm:2.0.5" + checksum: 10c0/4199f14a7a13da2177c66c31080008b7124331956f47bca57dd0b6ea9f11687aa25e565a2c7a2b519bc86988d10398e3049a1f5df13c9f6b7664154690ae79fd + languageName: node + linkType: hard + "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -7875,6 +8826,20 @@ __metadata: languageName: node linkType: hard +"iterator.prototype@npm:^1.1.5": + version: 1.1.5 + resolution: "iterator.prototype@npm:1.1.5" + dependencies: + define-data-property: "npm:^1.1.4" + es-object-atoms: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.6" + get-proto: "npm:^1.0.0" + has-symbols: "npm:^1.1.0" + set-function-name: "npm:^2.0.2" + checksum: 10c0/f7a262808e1b41049ab55f1e9c29af7ec1025a000d243b83edf34ce2416eedd56079b117fa59376bb4a724110690f13aa8427f2ee29a09eec63a7e72367626d0 + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -8037,6 +9002,18 @@ __metadata: languageName: node linkType: hard +"jsx-ast-utils@npm:^2.4.1 || ^3.0.0": + version: 3.3.5 + resolution: "jsx-ast-utils@npm:3.3.5" + dependencies: + array-includes: "npm:^3.1.6" + array.prototype.flat: "npm:^1.3.1" + object.assign: "npm:^4.1.4" + object.values: "npm:^1.1.6" + checksum: 10c0/a32679e9cb55469cb6d8bbc863f7d631b2c98b7fc7bf172629261751a6e7bc8da6ae374ddb74d5fbd8b06cf0eb4572287b259813d92b36e384024ed35e4c13e1 + languageName: node + linkType: hard + "jwa@npm:^2.0.1": version: 2.0.1 resolution: "jwa@npm:2.0.1" @@ -8446,7 +9423,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -8706,7 +9683,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.1.5, minimatch@npm:^3.1.1": +"minimatch@npm:3.1.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.5 resolution: "minimatch@npm:3.1.5" dependencies: @@ -8999,6 +9976,18 @@ __metadata: languageName: node linkType: hard +"node-exports-info@npm:^1.6.0": + version: 1.6.2 + resolution: "node-exports-info@npm:1.6.2" + dependencies: + array.prototype.flatmap: "npm:^1.3.3" + es-errors: "npm:^1.3.0" + object.entries: "npm:^1.1.9" + semver: "npm:^6.3.1" + checksum: 10c0/5278fab18e8c97f45275c51819c3078ca9488361e875bc01b680776a339d2fee1ca9c6fcfb972df14945a1b184cc9924a1d9ba87e90f6cb3422c7f5b6900f4bc + languageName: node + linkType: hard + "node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -9197,7 +10186,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4": +"object-assign@npm:^4, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414 @@ -9218,6 +10207,63 @@ __metadata: languageName: node linkType: hard +"object-keys@npm:^1.1.1": + version: 1.1.1 + resolution: "object-keys@npm:1.1.1" + checksum: 10c0/b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d + languageName: node + linkType: hard + +"object.assign@npm:^4.1.4, object.assign@npm:^4.1.7": + version: 4.1.7 + resolution: "object.assign@npm:4.1.7" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + has-symbols: "npm:^1.1.0" + object-keys: "npm:^1.1.1" + checksum: 10c0/3b2732bd860567ea2579d1567525168de925a8d852638612846bd8082b3a1602b7b89b67b09913cbb5b9bd6e95923b2ae73580baa9d99cb4e990564e8cbf5ddc + languageName: node + linkType: hard + +"object.entries@npm:^1.1.9": + version: 1.1.9 + resolution: "object.entries@npm:1.1.9" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.1.1" + checksum: 10c0/d4b8c1e586650407da03370845f029aa14076caca4e4d4afadbc69cfb5b78035fd3ee7be417141abdb0258fa142e59b11923b4c44d8b1255b28f5ffcc50da7db + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.8": + version: 2.0.8 + resolution: "object.fromentries@npm:2.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.2" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/cd4327e6c3369cfa805deb4cbbe919bfb7d3aeebf0bcaba291bb568ea7169f8f8cdbcabe2f00b40db0c20cd20f08e11b5f3a5a36fb7dd3fe04850c50db3bf83b + languageName: node + linkType: hard + +"object.values@npm:^1.1.6, object.values@npm:^1.2.1": + version: 1.2.1 + resolution: "object.values@npm:1.2.1" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/3c47814fdc64842ae3d5a74bc9d06bdd8d21563c04d9939bf6716a9c00596a4ebc342552f8934013d1ec991c74e3671b26710a0c51815f0b603795605ab6b2c9 + languageName: node + linkType: hard + "obug@npm:^2.1.1": version: 2.1.3 resolution: "obug@npm:2.1.3" @@ -9317,6 +10363,18 @@ __metadata: languageName: node linkType: hard +"own-keys@npm:^1.0.1": + version: 1.0.2 + resolution: "own-keys@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.4" + get-intrinsic: "npm:^1.3.0" + object-keys: "npm:^1.1.1" + safe-push-apply: "npm:^1.0.0" + checksum: 10c0/84b0d9959475231166c3d4b9abd1b8af8042911e2e5625761eed980d1a1e4381cf52b34d907cae21ee8766c79de943f3cd85eba636149dee5e64cceff3ccdb78 + languageName: node + linkType: hard + "p-limit@npm:^3.0.1, p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -9598,6 +10656,13 @@ __metadata: languageName: node linkType: hard +"possible-typed-array-names@npm:^1.0.0, possible-typed-array-names@npm:^1.1.0": + version: 1.1.0 + resolution: "possible-typed-array-names@npm:1.1.0" + checksum: 10c0/c810983414142071da1d644662ce4caebce890203eb2bc7bf119f37f3fe5796226e117e6cca146b521921fa6531072674174a3325066ac66fce089a53e1e5196 + languageName: node + linkType: hard + "postcss-media-query-parser@npm:^0.2.3": version: 0.2.3 resolution: "postcss-media-query-parser@npm:0.2.3" @@ -9683,6 +10748,17 @@ __metadata: languageName: node linkType: hard +"prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: "npm:^1.4.0" + object-assign: "npm:^4.1.1" + react-is: "npm:^16.13.1" + checksum: 10c0/59ece7ca2fb9838031d73a48d4becb9a7cc1ed10e610517c7d8f19a1e02fa47f7c27d557d8a5702bec3cfeccddc853579832b43f449e54635803f277b1c78077 + languageName: node + linkType: hard + "proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -9807,6 +10883,24 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^19.2.7": + version: 19.2.8 + resolution: "react-dom@npm:19.2.8" + dependencies: + scheduler: "npm:^0.27.0" + peerDependencies: + react: ^19.2.8 + checksum: 10c0/41ba2247b76f687fcfe5bbc99f514d6b851d8c8041c2f5ded36ed05bd7fdc5208cacbac9de51e3e6633e77f96f44cec9f0d4a5a55184dba4e00738f224439134 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 + languageName: node + linkType: hard + "react-remove-scroll-bar@npm:^2.3.7": version: 2.3.8 resolution: "react-remove-scroll-bar@npm:2.3.8" @@ -9877,6 +10971,13 @@ __metadata: languageName: node linkType: hard +"react@npm:^19.2.7": + version: 19.2.8 + resolution: "react@npm:19.2.8" + checksum: 10c0/5f86bdb56426652fd6d989d30a6f2e603c057272c47c9ca3a3fbe190a3a39ee9ccce937d63cfc039717abed1b8891d6a499134bc35311acc07eafdacd86537cd + languageName: node + linkType: hard + "readable-stream@npm:^3.1.1": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" @@ -9927,6 +11028,36 @@ __metadata: languageName: node linkType: hard +"reflect.getprototypeof@npm:^1.0.10, reflect.getprototypeof@npm:^1.0.9": + version: 1.0.10 + resolution: "reflect.getprototypeof@npm:1.0.10" + dependencies: + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.9" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.7" + get-proto: "npm:^1.0.1" + which-builtin-type: "npm:^1.2.1" + checksum: 10c0/7facec28c8008876f8ab98e80b7b9cb4b1e9224353fd4756dda5f2a4ab0d30fa0a5074777c6df24e1e0af463a2697513b0a11e548d99cf52f21f7bc6ba48d3ac + languageName: node + linkType: hard + +"regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": + version: 1.5.4 + resolution: "regexp.prototype.flags@npm:1.5.4" + dependencies: + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-errors: "npm:^1.3.0" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + set-function-name: "npm:^2.0.2" + checksum: 10c0/83b88e6115b4af1c537f8dabf5c3744032cb875d63bc05c288b1b8c0ef37cbe55353f95d8ca817e8843806e3e150b118bc624e4279b24b4776b4198232735a77 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -9955,6 +11086,22 @@ __metadata: languageName: node linkType: hard +"resolve@npm:^2.0.0-next.5": + version: 2.0.0-next.7 + resolution: "resolve@npm:2.0.0-next.7" + dependencies: + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.2" + node-exports-info: "npm:^1.6.0" + object-keys: "npm:^1.1.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/8c6fa17ccdb826a3a52387ed8c42bf705dbc19d5494da52a86661b124b5b88fad5d8edbbb4434a1b563ecf7768368b7da9fb9489e20f36e02f7551a4ea87d707 + languageName: node + linkType: hard + "resolve@patch:resolve@npm%3A^1.1.6#optional!builtin": version: 1.22.12 resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" @@ -9969,6 +11116,22 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin": + version: 2.0.0-next.7 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.7#optional!builtin::version=2.0.0-next.7&hash=c3c19d" + dependencies: + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.2" + node-exports-info: "npm:^1.6.0" + object-keys: "npm:^1.1.1" + path-parse: "npm:^1.0.7" + supports-preserve-symlinks-flag: "npm:^1.0.0" + bin: + resolve: bin/resolve + checksum: 10c0/6bb6f1d8a1789f7a5b4e35d950e76bc0ba632ff7d51fe86b6f34fb5f41e1ce0f7b884ec166828cfc0728ddffeaee49527711d752d12398297f90c51acd22195e + languageName: node + linkType: hard + "restore-cursor@npm:^5.0.0": version: 5.1.0 resolution: "restore-cursor@npm:5.1.0" @@ -10296,6 +11459,19 @@ __metadata: languageName: node linkType: hard +"safe-array-concat@npm:^1.1.3": + version: 1.1.4 + resolution: "safe-array-concat@npm:1.1.4" + dependencies: + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + get-intrinsic: "npm:^1.3.0" + has-symbols: "npm:^1.1.0" + isarray: "npm:^2.0.5" + checksum: 10c0/95fb4904ab1d9360a666fe5ba6d88f1c4a3a39682739e4512cff809fc6b5722a94bd95189211015bfb45859a7ffbc3340ea303ae22721c91c59e8946d310975a + languageName: node + linkType: hard + "safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" @@ -10303,6 +11479,27 @@ __metadata: languageName: node linkType: hard +"safe-push-apply@npm:^1.0.0": + version: 1.0.0 + resolution: "safe-push-apply@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + isarray: "npm:^2.0.5" + checksum: 10c0/831f1c9aae7436429e7862c7e46f847dfe490afac20d0ee61bae06108dbf5c745a0de3568ada30ccdd3eeb0864ca8331b2eef703abd69bfea0745b21fd320750 + languageName: node + linkType: hard + +"safe-regex-test@npm:^1.1.0": + version: 1.1.0 + resolution: "safe-regex-test@npm:1.1.0" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + is-regex: "npm:^1.2.1" + checksum: 10c0/f2c25281bbe5d39cddbbce7f86fca5ea9b3ce3354ea6cd7c81c31b006a5a9fff4286acc5450a3b9122c56c33eba69c56b9131ad751457b2b4a585825e6a10665 + languageName: node + linkType: hard + "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" @@ -10369,6 +11566,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.27.0": + version: 0.27.0 + resolution: "scheduler@npm:0.27.0" + checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 + languageName: node + linkType: hard + "semver@npm:7.7.4": version: 7.7.4 resolution: "semver@npm:7.7.4" @@ -10405,6 +11609,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.8.5 + resolution: "semver@npm:7.8.5" + bin: + semver: bin/semver.js + checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c + languageName: node + linkType: hard + "send@npm:^1.1.0, send@npm:^1.2.0": version: 1.2.1 resolution: "send@npm:1.2.1" @@ -10451,6 +11664,43 @@ __metadata: languageName: node linkType: hard +"set-function-length@npm:^1.2.2": + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" + dependencies: + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" + gopd: "npm:^1.0.1" + has-property-descriptors: "npm:^1.0.2" + checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c + languageName: node + linkType: hard + +"set-function-name@npm:^2.0.2": + version: 2.0.2 + resolution: "set-function-name@npm:2.0.2" + dependencies: + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + functions-have-names: "npm:^1.2.3" + has-property-descriptors: "npm:^1.0.2" + checksum: 10c0/fce59f90696c450a8523e754abb305e2b8c73586452619c2bad5f7bf38c7b6b4651895c9db895679c5bef9554339cf3ef1c329b66ece3eda7255785fbe299316 + languageName: node + linkType: hard + +"set-proto@npm:^1.0.0": + version: 1.0.0 + resolution: "set-proto@npm:1.0.0" + dependencies: + dunder-proto: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/ca5c3ccbba479d07c30460e367e66337cec825560b11e8ba9c5ebe13a2a0d6021ae34eddf94ff3dfe17a3104dc1f191519cb6c48378b503e5c3f36393938776a + languageName: node + linkType: hard + "setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" @@ -10762,6 +12012,16 @@ __metadata: languageName: node linkType: hard +"stop-iteration-iterator@npm:^1.1.0": + version: 1.1.0 + resolution: "stop-iteration-iterator@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + internal-slot: "npm:^1.1.0" + checksum: 10c0/de4e45706bb4c0354a4b1122a2b8cc45a639e86206807ce0baf390ee9218d3ef181923fa4d2b67443367c491aa255c5fbaa64bb74648e3c5b48299928af86c09 + languageName: node + linkType: hard + "stream-events@npm:^1.0.5": version: 1.0.5 resolution: "stream-events@npm:1.0.5" @@ -10821,6 +12081,76 @@ __metadata: languageName: node linkType: hard +"string.prototype.matchall@npm:^4.0.12": + version: 4.0.12 + resolution: "string.prototype.matchall@npm:4.0.12" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.3" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.6" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + get-intrinsic: "npm:^1.2.6" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + internal-slot: "npm:^1.1.0" + regexp.prototype.flags: "npm:^1.5.3" + set-function-name: "npm:^2.0.2" + side-channel: "npm:^1.1.0" + checksum: 10c0/1a53328ada73f4a77f1fdf1c79414700cf718d0a8ef6672af5603e709d26a24f2181208144aed7e858b1bcc1a0d08567a570abfb45567db4ae47637ed2c2f85c + languageName: node + linkType: hard + +"string.prototype.repeat@npm:^1.0.0": + version: 1.0.0 + resolution: "string.prototype.repeat@npm:1.0.0" + dependencies: + define-properties: "npm:^1.1.3" + es-abstract: "npm:^1.17.5" + checksum: 10c0/94c7978566cffa1327d470fd924366438af9b04b497c43a9805e476e2e908aa37a1fd34cc0911156c17556dab62159d12c7b92b3cc304c3e1281fe4c8e668f40 + languageName: node + linkType: hard + +"string.prototype.trim@npm:^1.2.10": + version: 1.2.11 + resolution: "string.prototype.trim@npm:1.2.11" + dependencies: + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + define-data-property: "npm:^1.1.4" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.24.2" + es-object-atoms: "npm:^1.1.2" + has-property-descriptors: "npm:^1.0.2" + safe-regex-test: "npm:^1.1.0" + checksum: 10c0/b153cf8ed06db82ff40e27829e88e5c13f45eff9799f1d5707626e25989b488b059d6f5d57011e07f77745e28451e16735f295bc59c8ae146a4fd73a442366b0 + languageName: node + linkType: hard + +"string.prototype.trimend@npm:^1.0.9": + version: 1.0.10 + resolution: "string.prototype.trimend@npm:1.0.10" + dependencies: + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.1.2" + checksum: 10c0/cc09233181769047a5330becfd5740fec5f0c8137886e7b553626788b00f75df9f34db1159bc52dbb7fc389b8ebb6e1dab44c8c9e31eb600039729a542013286 + languageName: node + linkType: hard + +"string.prototype.trimstart@npm:^1.0.8": + version: 1.0.8 + resolution: "string.prototype.trimstart@npm:1.0.8" + dependencies: + call-bind: "npm:^1.0.7" + define-properties: "npm:^1.2.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/d53af1899959e53c83b64a5fd120be93e067da740e7e75acb433849aa640782fb6c7d4cd5b84c954c84413745a3764df135a8afeb22908b86a835290788d8366 + languageName: node + linkType: hard + "string_decoder@npm:^1.1.1": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" @@ -11057,6 +12387,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.5.0": + version: 2.5.0 + resolution: "ts-api-utils@npm:2.5.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: 10c0/767849383c114e7f1971fa976b20e73ac28fd0c70d8d65c0004790bf4d8f89888c7e4cf6d5949f9c1beae9bc3c64835bef77bbe27fddf45a3c7b60cebcf85c8c + languageName: node + linkType: hard + "ts-node@npm:^10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" @@ -11148,6 +12487,74 @@ __metadata: languageName: node linkType: hard +"typed-array-buffer@npm:^1.0.3": + version: 1.0.3 + resolution: "typed-array-buffer@npm:1.0.3" + dependencies: + call-bound: "npm:^1.0.3" + es-errors: "npm:^1.3.0" + is-typed-array: "npm:^1.1.14" + checksum: 10c0/1105071756eb248774bc71646bfe45b682efcad93b55532c6ffa4518969fb6241354e4aa62af679ae83899ec296d69ef88f1f3763657cdb3a4d29321f7b83079 + languageName: node + linkType: hard + +"typed-array-byte-length@npm:^1.0.3": + version: 1.0.3 + resolution: "typed-array-byte-length@npm:1.0.3" + dependencies: + call-bind: "npm:^1.0.8" + for-each: "npm:^0.3.3" + gopd: "npm:^1.2.0" + has-proto: "npm:^1.2.0" + is-typed-array: "npm:^1.1.14" + checksum: 10c0/6ae083c6f0354f1fce18b90b243343b9982affd8d839c57bbd2c174a5d5dc71be9eb7019ffd12628a96a4815e7afa85d718d6f1e758615151d5f35df841ffb3e + languageName: node + linkType: hard + +"typed-array-byte-offset@npm:^1.0.4": + version: 1.0.4 + resolution: "typed-array-byte-offset@npm:1.0.4" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + for-each: "npm:^0.3.3" + gopd: "npm:^1.2.0" + has-proto: "npm:^1.2.0" + is-typed-array: "npm:^1.1.15" + reflect.getprototypeof: "npm:^1.0.9" + checksum: 10c0/3d805b050c0c33b51719ee52de17c1cd8e6a571abdf0fffb110e45e8dd87a657e8b56eee94b776b13006d3d347a0c18a730b903cf05293ab6d92e99ff8f77e53 + languageName: node + linkType: hard + +"typed-array-length@npm:^1.0.7": + version: 1.0.8 + resolution: "typed-array-length@npm:1.0.8" + dependencies: + call-bind: "npm:^1.0.9" + for-each: "npm:^0.3.5" + gopd: "npm:^1.2.0" + is-typed-array: "npm:^1.1.15" + possible-typed-array-names: "npm:^1.1.0" + reflect.getprototypeof: "npm:^1.0.10" + checksum: 10c0/5319f740fc426a3217182c2f7c87656acb0903e046de5a938e30167337d26abf1bb3ad4b32833a72521a4cc58223aec80627b38b357d0a3d5fd64881427e77ab + languageName: node + linkType: hard + +"typescript-eslint@npm:^8.61.0": + version: 8.67.0 + resolution: "typescript-eslint@npm:8.67.0" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.67.0" + "@typescript-eslint/parser": "npm:8.67.0" + "@typescript-eslint/typescript-estree": "npm:8.67.0" + "@typescript-eslint/utils": "npm:8.67.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.1.0" + checksum: 10c0/6ec860f6763be8a5fabf143ff8b6d4e43b98be0c781afc4725eda578ba0e0cecfcc1f06bd1b56fcccc41fa3fc15e4661ec32120d28f2e7abe4953e91ee346de1 + languageName: node + linkType: hard + "typescript@npm:5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -11175,6 +12582,18 @@ __metadata: languageName: node linkType: hard +"unbox-primitive@npm:^1.1.0": + version: 1.1.0 + resolution: "unbox-primitive@npm:1.1.0" + dependencies: + call-bound: "npm:^1.0.3" + has-bigints: "npm:^1.0.2" + has-symbols: "npm:^1.1.0" + which-boxed-primitive: "npm:^1.1.1" + checksum: 10c0/7dbd35ab02b0e05fe07136c72cb9355091242455473ec15057c11430129bab38b7b3624019b8778d02a881c13de44d63cd02d122ee782fb519e1de7775b5b982 + languageName: node + linkType: hard + "undici-types@npm:>=7.24.0 <7.24.7": version: 7.24.6 resolution: "undici-types@npm:7.24.6" @@ -11545,6 +12964,67 @@ __metadata: languageName: node linkType: hard +"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": + version: 1.1.1 + resolution: "which-boxed-primitive@npm:1.1.1" + dependencies: + is-bigint: "npm:^1.1.0" + is-boolean-object: "npm:^1.2.1" + is-number-object: "npm:^1.1.1" + is-string: "npm:^1.1.1" + is-symbol: "npm:^1.1.1" + checksum: 10c0/aceea8ede3b08dede7dce168f3883323f7c62272b49801716e8332ff750e7ae59a511ae088840bc6874f16c1b7fd296c05c949b0e5b357bfe3c431b98c417abe + languageName: node + linkType: hard + +"which-builtin-type@npm:^1.2.1": + version: 1.2.1 + resolution: "which-builtin-type@npm:1.2.1" + dependencies: + call-bound: "npm:^1.0.2" + function.prototype.name: "npm:^1.1.6" + has-tostringtag: "npm:^1.0.2" + is-async-function: "npm:^2.0.0" + is-date-object: "npm:^1.1.0" + is-finalizationregistry: "npm:^1.1.0" + is-generator-function: "npm:^1.0.10" + is-regex: "npm:^1.2.1" + is-weakref: "npm:^1.0.2" + isarray: "npm:^2.0.5" + which-boxed-primitive: "npm:^1.1.0" + which-collection: "npm:^1.0.2" + which-typed-array: "npm:^1.1.16" + checksum: 10c0/8dcf323c45e5c27887800df42fbe0431d0b66b1163849bb7d46b5a730ad6a96ee8bfe827d078303f825537844ebf20c02459de41239a0a9805e2fcb3cae0d471 + languageName: node + linkType: hard + +"which-collection@npm:^1.0.2": + version: 1.0.2 + resolution: "which-collection@npm:1.0.2" + dependencies: + is-map: "npm:^2.0.3" + is-set: "npm:^2.0.3" + is-weakmap: "npm:^2.0.2" + is-weakset: "npm:^2.0.3" + checksum: 10c0/3345fde20964525a04cdf7c4a96821f85f0cc198f1b2ecb4576e08096746d129eb133571998fe121c77782ac8f21cbd67745a3d35ce100d26d4e684c142ea1f2 + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": + version: 1.1.22 + resolution: "which-typed-array@npm:1.1.22" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.9" + call-bound: "npm:^1.0.4" + for-each: "npm:^0.3.5" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-tostringtag: "npm:^1.0.2" + checksum: 10c0/e59db184a4e78b461fac3b05fafc1e7badbbedafbf04a967ee1de73717f1f9723a79699e7b5de71d449541cb5da8353efc01a4f8a72a152479850e54fa196c40 + languageName: node + linkType: hard + "which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -11814,7 +13294,7 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.25.1": +"zod-to-json-schema@npm:^3.25.1, zod-to-json-schema@npm:^3.25.2": version: 3.25.2 resolution: "zod-to-json-schema@npm:3.25.2" peerDependencies: @@ -11823,6 +13303,15 @@ __metadata: languageName: node linkType: hard +"zod-validation-error@npm:^3.5.0 || ^4.0.0": + version: 4.0.2 + resolution: "zod-validation-error@npm:4.0.2" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + checksum: 10c0/0ccfec48c46de1be440b719cd02044d4abb89ed0e14c13e637cd55bf29102f67ccdba373f25def0fc7130e5f15025be4d557a7edcc95d5a3811599aade689e1b + languageName: node + linkType: hard + "zod@npm:^3.25.76": version: 3.25.76 resolution: "zod@npm:3.25.76" From f0b926a6a19479d1c403b9b39133edd1a46d80cf Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 10:59:28 +0930 Subject: [PATCH 18/26] style(samples): add license headers to community template sample YAML files --- .../templates/templates/feedback_item.yaml | 14 ++++++++++++++ .../community/templates/templates/goal_item.yaml | 14 ++++++++++++++ .../community/templates/templates/salary_card.yaml | 14 ++++++++++++++ .../templates/templates/section_card.yaml | 14 ++++++++++++++ .../community/templates/templates/team_card.yaml | 14 ++++++++++++++ .../templates/templates/team_feedback_board.yaml | 14 ++++++++++++++ .../templates/templates/team_goal_list.yaml | 14 ++++++++++++++ .../templates/team_member_knowledge_panel.yaml | 14 ++++++++++++++ .../community/templates/templates/team_roster.yaml | 14 ++++++++++++++ .../templates/templates/two_column_layout.yaml | 14 ++++++++++++++ .../templates/templates/user_profile.yaml | 14 ++++++++++++++ 11 files changed, 154 insertions(+) diff --git a/samples/community/templates/templates/feedback_item.yaml b/samples/community/templates/templates/feedback_item.yaml index bb170c2333..a5baad6a6e 100644 --- a/samples/community/templates/templates/feedback_item.yaml +++ b/samples/community/templates/templates/feedback_item.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: FeedbackItem description: Card showing feedback note, author, and rating. diff --git a/samples/community/templates/templates/goal_item.yaml b/samples/community/templates/templates/goal_item.yaml index a9009555b0..06b3d4d3ae 100644 --- a/samples/community/templates/templates/goal_item.yaml +++ b/samples/community/templates/templates/goal_item.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: GoalItem description: Card showing an individual objective with priority, title, target date, and action button. diff --git a/samples/community/templates/templates/salary_card.yaml b/samples/community/templates/templates/salary_card.yaml index f4b847e746..b1959b71b8 100644 --- a/samples/community/templates/templates/salary_card.yaml +++ b/samples/community/templates/templates/salary_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: SalaryCard description: Layout card for employee compensation package with security verification badge. diff --git a/samples/community/templates/templates/section_card.yaml b/samples/community/templates/templates/section_card.yaml index 7511911d03..34ade454b4 100644 --- a/samples/community/templates/templates/section_card.yaml +++ b/samples/community/templates/templates/section_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: SectionCard description: Standard container card with title, description, optional action, and child components. diff --git a/samples/community/templates/templates/team_card.yaml b/samples/community/templates/templates/team_card.yaml index ec751df98d..d7f47ab03f 100644 --- a/samples/community/templates/templates/team_card.yaml +++ b/samples/community/templates/templates/team_card.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamCard description: Card showing team header and unrolled member cards. diff --git a/samples/community/templates/templates/team_feedback_board.yaml b/samples/community/templates/templates/team_feedback_board.yaml index 07efe56f33..d7f874dac5 100644 --- a/samples/community/templates/templates/team_feedback_board.yaml +++ b/samples/community/templates/templates/team_feedback_board.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamFeedbackBoard description: Feedback board showing team header and feedback items. diff --git a/samples/community/templates/templates/team_goal_list.yaml b/samples/community/templates/templates/team_goal_list.yaml index e2afc5fccc..5a7c092c66 100644 --- a/samples/community/templates/templates/team_goal_list.yaml +++ b/samples/community/templates/templates/team_goal_list.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamGoalList description: List of team goals with header banner. diff --git a/samples/community/templates/templates/team_member_knowledge_panel.yaml b/samples/community/templates/templates/team_member_knowledge_panel.yaml index 925788ea82..4d68cd0258 100644 --- a/samples/community/templates/templates/team_member_knowledge_panel.yaml +++ b/samples/community/templates/templates/team_member_knowledge_panel.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamMemberKnowledgePanel description: Card showing competency panel with user experience years and completed task count. diff --git a/samples/community/templates/templates/team_roster.yaml b/samples/community/templates/templates/team_roster.yaml index 1abfb112a7..f0a96d0c0c 100644 --- a/samples/community/templates/templates/team_roster.yaml +++ b/samples/community/templates/templates/team_roster.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TeamRoster description: Team directory roster with styled header banner and nested team lists. diff --git a/samples/community/templates/templates/two_column_layout.yaml b/samples/community/templates/templates/two_column_layout.yaml index 78115c7eaf..c2d1866885 100644 --- a/samples/community/templates/templates/two_column_layout.yaml +++ b/samples/community/templates/templates/two_column_layout.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: TwoColumnLayout description: Responsive two-column dashboard layout with banner header and split content regions. diff --git a/samples/community/templates/templates/user_profile.yaml b/samples/community/templates/templates/user_profile.yaml index ec722b559e..517215afa2 100644 --- a/samples/community/templates/templates/user_profile.yaml +++ b/samples/community/templates/templates/user_profile.yaml @@ -1,3 +1,17 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: "0.1" templateId: UserProfile description: User profile card displaying avatar, full name, and role. From 194bf10e868630236b1db179d9877a0de04531e3 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:24:24 +0930 Subject: [PATCH 19/26] feat(samples): declare catalogs and name in community sample templates --- samples/community/templates/server.py | 12 ++++---- .../templates/templates/feedback_item.yaml | 6 ++-- .../templates/templates/goal_item.yaml | 9 +++--- .../templates/templates/salary_card.yaml | 29 ++++++++++--------- .../templates/templates/section_card.yaml | 9 +++--- .../templates/templates/team_card.yaml | 6 ++-- .../templates/team_feedback_board.yaml | 12 ++++---- .../templates/templates/team_goal_list.yaml | 6 ++-- .../team_member_knowledge_panel.yaml | 13 +++++---- .../templates/templates/team_roster.yaml | 6 ++-- .../templates/two_column_layout.yaml | 9 +++--- .../templates/templates/user_profile.yaml | 6 ++-- 12 files changed, 66 insertions(+), 57 deletions(-) diff --git a/samples/community/templates/server.py b/samples/community/templates/server.py index a2c56e1b1e..95cfc9302a 100644 --- a/samples/community/templates/server.py +++ b/samples/community/templates/server.py @@ -18,7 +18,6 @@ import glob import inspect -import json import os from pathlib import Path import time @@ -30,11 +29,8 @@ from pydantic import BaseModel from a2ui.inference_formats.experimental.template import ( - Template, StaticTemplate, DynamicTemplate, - Param, - ParamType, TemplateInferenceFormat, ) @@ -245,7 +241,7 @@ def load_templates() -> List[Any]: for path in glob.glob(templates_pattern): loaded_templates = StaticTemplate.from_yaml_file(path) for tmpl in loaded_templates: - if tmpl.template_id == "SalaryCard": + if tmpl.name == "SalaryCard" or tmpl.template_id == "SalaryCard": salary_layout = tmpl else: templates_list.append(tmpl) @@ -254,7 +250,11 @@ def load_templates() -> List[Any]: # Register DynamicTemplate for EmployeeSalaryCard (Data Binding Mode) dynamic_salary = DynamicTemplate( version="0.1", + name="EmployeeSalaryCard", template_id="EmployeeSalaryCard", + catalogs=[ + "https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json" + ], resolver=fetch_employee_compensation, layout=salary_layout, description=( @@ -270,7 +270,9 @@ def load_templates() -> List[Any]: # Register DynamicTemplate for PayrollSummary (Programmatic Render Mode) payroll_template = DynamicTemplate( version="0.1", + name="PayrollSummary", template_id="PayrollSummary", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], render=render_payroll_summary, description=( "Programmatic dynamic template that aggregates company payroll, sums" diff --git a/samples/community/templates/templates/feedback_item.yaml b/samples/community/templates/templates/feedback_item.yaml index a5baad6a6e..81f09b5016 100644 --- a/samples/community/templates/templates/feedback_item.yaml +++ b/samples/community/templates/templates/feedback_item.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: FeedbackItem description: Card showing feedback note, author, and rating. - parameters: author: type: string @@ -32,7 +31,6 @@ parameters: minimum: 1 maximum: 5 default: 5 - layout: component: Card child: @@ -53,8 +51,10 @@ layout: - component: Text text: "Rating: ${rating}/5" variant: caption - sampleData: author: Dr. Elena Vance note: A2UI templates are fast and easy to compose. rating: 5 +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: FeedbackItem diff --git a/samples/community/templates/templates/goal_item.yaml b/samples/community/templates/templates/goal_item.yaml index 06b3d4d3ae..838540b756 100644 --- a/samples/community/templates/templates/goal_item.yaml +++ b/samples/community/templates/templates/goal_item.yaml @@ -14,8 +14,8 @@ version: "0.1" templateId: GoalItem -description: Card showing an individual objective with priority, title, target date, and action button. - +description: Card showing an individual objective with priority, title, target date, + and action button. parameters: title: type: string @@ -35,7 +35,6 @@ parameters: title: Target Date description: Target completion date in YYYY-MM-DD format. default: "2026-12-31" - layout: component: Card child: @@ -66,8 +65,10 @@ layout: child: component: Text text: View Details - sampleData: title: Launch A2UI SDK v1.0 priority: High targetDate: "2026-09-30" +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: GoalItem diff --git a/samples/community/templates/templates/salary_card.yaml b/samples/community/templates/templates/salary_card.yaml index b1959b71b8..4b09175453 100644 --- a/samples/community/templates/templates/salary_card.yaml +++ b/samples/community/templates/templates/salary_card.yaml @@ -14,8 +14,8 @@ version: "0.1" templateId: SalaryCard -description: Layout card for employee compensation package with security verification badge. - +description: Layout card for employee compensation package with security verification + badge. parameters: employeeName: type: string @@ -41,13 +41,12 @@ parameters: type: string title: Security Clearance description: Confidentiality level. - default: "Level 4 - Confidential" + default: Level 4 - Confidential verifiedAt: type: string title: Verification Date description: Timestamp of record retrieval. default: "2026-08-13" - layout: component: Card child: @@ -63,7 +62,7 @@ layout: - component: Icon name: lock - component: Text - text: "Verified Compensation" + text: Verified Compensation variant: caption - component: Text text: ${clearanceLevel} @@ -85,7 +84,7 @@ layout: - component: Column children: - component: Text - text: "Base Salary" + text: Base Salary variant: caption - component: Text text: ${baseSalary} @@ -94,7 +93,7 @@ layout: - component: Column children: - component: Text - text: "Annual Bonus" + text: Annual Bonus variant: caption - component: Text text: ${annualBonus} @@ -102,7 +101,7 @@ layout: - component: Column children: - component: Text - text: "Equity Grants" + text: Equity Grants variant: caption - component: Text text: ${equity} @@ -114,17 +113,19 @@ layout: align: center children: - component: Text - text: "🔒 Fetched live from secure HR database" + text: "\U0001F512 Fetched live from secure HR database" variant: caption - component: Text text: "Verified: ${verifiedAt}" variant: caption - sampleData: employeeName: Dr. Elena Vance role: Principal Systems Architect - baseSalary: "$215,000" - annualBonus: "$45,000" - equity: "3,500 RSUs" - clearanceLevel: "Level 5 - Confidential" + baseSalary: $215,000 + annualBonus: $45,000 + equity: 3,500 RSUs + clearanceLevel: Level 5 - Confidential verifiedAt: "2026-08-13" +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: SalaryCard diff --git a/samples/community/templates/templates/section_card.yaml b/samples/community/templates/templates/section_card.yaml index 34ade454b4..c6b7fd911b 100644 --- a/samples/community/templates/templates/section_card.yaml +++ b/samples/community/templates/templates/section_card.yaml @@ -14,8 +14,8 @@ version: "0.1" templateId: SectionCard -description: Standard container card with title, description, optional action, and child components. - +description: Standard container card with title, description, optional action, and + child components. parameters: title: type: string @@ -35,7 +35,6 @@ parameters: title: Section Children description: Child component IDs rendered in the section body. default: [] - layout: component: Card child: @@ -59,8 +58,10 @@ layout: - component: Column id: body_container children: ${children} - sampleData: title: Protocol Overview description: High-level summary of the streaming architecture. children: [] +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: SectionCard diff --git a/samples/community/templates/templates/team_card.yaml b/samples/community/templates/templates/team_card.yaml index d7f47ab03f..a2414c6da8 100644 --- a/samples/community/templates/templates/team_card.yaml +++ b/samples/community/templates/templates/team_card.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: TeamCard description: Card showing team header and unrolled member cards. - parameters: teamName: type: string @@ -45,7 +44,6 @@ parameters: required: - userId - userName - layout: component: Card child: @@ -67,7 +65,6 @@ layout: loop: param: members template: UserProfile - sampleData: teamName: Antigravity Devs members: @@ -80,3 +77,6 @@ sampleData: - userId: usr_103 userName: Charlie Brown role: Product Manager +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TeamCard diff --git a/samples/community/templates/templates/team_feedback_board.yaml b/samples/community/templates/templates/team_feedback_board.yaml index d7f874dac5..cd7bc4216d 100644 --- a/samples/community/templates/templates/team_feedback_board.yaml +++ b/samples/community/templates/templates/team_feedback_board.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: TeamFeedbackBoard description: Feedback board showing team header and feedback items. - parameters: teamName: type: string @@ -46,7 +45,6 @@ parameters: required: - author - note - layout: component: Column children: @@ -66,13 +64,17 @@ layout: loop: param: feedbacks template: FeedbackItem - sampleData: teamName: Streaming & Protocols Guild feedbacks: - author: Dr. Elena Vance - note: Splitting createSurface and updateComponents cleanly unblocked sequential stream processing. + note: Splitting createSurface and updateComponents cleanly unblocked sequential + stream processing. rating: 5 - author: Marcus Vance - note: Unrolling nested lists statically in Python enables instant frontend mounting with zero runtime recursion. + note: Unrolling nested lists statically in Python enables instant frontend mounting + with zero runtime recursion. rating: 5 +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TeamFeedbackBoard diff --git a/samples/community/templates/templates/team_goal_list.yaml b/samples/community/templates/templates/team_goal_list.yaml index 5a7c092c66..3821c0aa0c 100644 --- a/samples/community/templates/templates/team_goal_list.yaml +++ b/samples/community/templates/templates/team_goal_list.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: TeamGoalList description: List of team goals with header banner. - parameters: teamName: type: string @@ -47,7 +46,6 @@ parameters: description: Target completion date (YYYY-MM-DD) required: - title - layout: component: Column children: @@ -67,7 +65,6 @@ layout: loop: param: goals template: GoalItem - sampleData: teamName: A2UI Core Team goals: @@ -77,3 +74,6 @@ sampleData: - title: Upgrade all templates to standard Basic Catalog components priority: High targetDate: "2026-07-10" +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TeamGoalList diff --git a/samples/community/templates/templates/team_member_knowledge_panel.yaml b/samples/community/templates/templates/team_member_knowledge_panel.yaml index 4d68cd0258..1fec29bc46 100644 --- a/samples/community/templates/templates/team_member_knowledge_panel.yaml +++ b/samples/community/templates/templates/team_member_knowledge_panel.yaml @@ -14,8 +14,8 @@ version: "0.1" templateId: TeamMemberKnowledgePanel -description: Card showing competency panel with user experience years and completed task count. - +description: Card showing competency panel with user experience years and completed + task count. parameters: userName: type: string @@ -35,7 +35,6 @@ parameters: title: Completed Tasks Count description: The count of completed tasks or tickets. minimum: 0 - layout: component: Card child: @@ -70,7 +69,7 @@ layout: text: Experience variant: caption - component: Text - text: "${experienceYears} Yrs" + text: ${experienceYears} Yrs variant: body - component: Column align: center @@ -79,14 +78,16 @@ layout: text: Tasks variant: caption - component: Text - text: "${completedTasks} Done" + text: ${completedTasks} Done variant: body - component: Text text: Verified Core Contributor variant: caption - sampleData: userName: Alice Smith role: Systems Architect experienceYears: 9 completedTasks: 142 +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TeamMemberKnowledgePanel diff --git a/samples/community/templates/templates/team_roster.yaml b/samples/community/templates/templates/team_roster.yaml index f0a96d0c0c..a41689f5c0 100644 --- a/samples/community/templates/templates/team_roster.yaml +++ b/samples/community/templates/templates/team_roster.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: TeamRoster description: Team directory roster with styled header banner and nested team lists. - parameters: directoryTitle: type: string @@ -26,7 +25,6 @@ parameters: title: Team Cards List description: List of child TeamCard components. default: [] - layout: component: Column children: @@ -37,7 +35,9 @@ layout: axis: horizontal - component: Column children: ${children} - sampleData: directoryTitle: Global Engineering Directory children: [] +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TeamRoster diff --git a/samples/community/templates/templates/two_column_layout.yaml b/samples/community/templates/templates/two_column_layout.yaml index c2d1866885..19e744501c 100644 --- a/samples/community/templates/templates/two_column_layout.yaml +++ b/samples/community/templates/templates/two_column_layout.yaml @@ -14,8 +14,8 @@ version: "0.1" templateId: TwoColumnLayout -description: Responsive two-column dashboard layout with banner header and split content regions. - +description: Responsive two-column dashboard layout with banner header and split content + regions. parameters: headerChild: type: child @@ -31,7 +31,6 @@ parameters: title: Right Column Children description: Child components placed in the right secondary region. default: [] - layout: component: Column children: @@ -45,10 +44,12 @@ layout: children: ${leftChildren} - component: Column children: ${rightChildren} - sampleData: headerChild: component: Text text: Header Title leftChildren: [] rightChildren: [] +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: TwoColumnLayout diff --git a/samples/community/templates/templates/user_profile.yaml b/samples/community/templates/templates/user_profile.yaml index 517215afa2..87c2bb406b 100644 --- a/samples/community/templates/templates/user_profile.yaml +++ b/samples/community/templates/templates/user_profile.yaml @@ -15,7 +15,6 @@ version: "0.1" templateId: UserProfile description: User profile card displaying avatar, full name, and role. - parameters: userId: type: string @@ -30,7 +29,6 @@ parameters: title: Role description: Job title or role. default: Member - layout: component: Card child: @@ -45,8 +43,10 @@ layout: - component: Text text: ${role} variant: caption - sampleData: userId: usr_101 userName: Alice Smith role: Lead Architect +catalogs: + - https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json +name: UserProfile From b8836577e887603c06a26f79f7107a20fa8ac657 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:35:42 +0930 Subject: [PATCH 20/26] feat(samples): use @dynamic_template decorator in community sample server --- samples/community/templates/server.py | 30 +++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/samples/community/templates/server.py b/samples/community/templates/server.py index 95cfc9302a..abd1d6c2a5 100644 --- a/samples/community/templates/server.py +++ b/samples/community/templates/server.py @@ -31,6 +31,7 @@ from a2ui.inference_formats.experimental.template import ( StaticTemplate, DynamicTemplate, + dynamic_template, TemplateInferenceFormat, ) @@ -98,6 +99,17 @@ def fetch_employee_compensation(employeeId: str) -> Dict[str, Any]: return EMPLOYEE_COMPENSATION_DB[employeeId] +@dynamic_template( + name="PayrollSummary", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + description=( + "Programmatic dynamic template that aggregates company payroll, sums" + " employee base salaries and bonuses using server Python logic, and builds" + " an interactive summary table. Pass department (default 'Engineering') and" + " includeBonus (default True)." + ), + sample_data={"department": "Global Engineering", "includeBonus": True}, +) def render_payroll_summary( department: str = "Global Engineering", includeBonus: bool = True ) -> Dict[str, Any]: @@ -267,22 +279,8 @@ def load_templates() -> List[Any]: ) templates_list.append(dynamic_salary) - # Register DynamicTemplate for PayrollSummary (Programmatic Render Mode) - payroll_template = DynamicTemplate( - version="0.1", - name="PayrollSummary", - template_id="PayrollSummary", - catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], - render=render_payroll_summary, - description=( - "Programmatic dynamic template that aggregates company payroll, sums" - " employee base salaries and bonuses using server Python logic, and builds" - " an interactive summary table. Pass department (default 'Engineering') and" - " includeBonus (default True)." - ), - sample_data={"department": "Global Engineering", "includeBonus": True}, - ) - templates_list.append(payroll_template) + # Register DynamicTemplate for PayrollSummary (Programmatic Render Mode via decorator) + templates_list.append(render_payroll_summary) return templates_list From d745da8d9ecb602783f67946d2622fc1061a0636 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:30:55 +0930 Subject: [PATCH 21/26] feat(samples): adopt double-curly Mustache syntax in community templates --- .../templates/templates/feedback_item.yaml | 8 +++++--- .../templates/templates/goal_item.yaml | 7 ++++--- .../templates/templates/salary_card.yaml | 20 ++++++++++++------- .../templates/templates/section_card.yaml | 12 +++++++---- .../templates/templates/team_card.yaml | 3 ++- .../templates/team_feedback_board.yaml | 2 +- .../templates/templates/team_goal_list.yaml | 2 +- .../team_member_knowledge_panel.yaml | 11 ++++++---- .../templates/templates/team_roster.yaml | 6 ++++-- .../templates/two_column_layout.yaml | 9 ++++++--- .../templates/templates/user_profile.yaml | 6 ++++-- 11 files changed, 55 insertions(+), 31 deletions(-) diff --git a/samples/community/templates/templates/feedback_item.yaml b/samples/community/templates/templates/feedback_item.yaml index 81f09b5016..a68544221a 100644 --- a/samples/community/templates/templates/feedback_item.yaml +++ b/samples/community/templates/templates/feedback_item.yaml @@ -37,7 +37,8 @@ layout: component: Column children: - component: Text - text: ${note} + text: "{{ note }}" + variant: body - component: Divider axis: horizontal @@ -46,10 +47,11 @@ layout: align: center children: - component: Text - text: ${author} + text: "{{ author }}" + variant: caption - component: Text - text: "Rating: ${rating}/5" + text: "Rating: {{ rating }}/5" variant: caption sampleData: author: Dr. Elena Vance diff --git a/samples/community/templates/templates/goal_item.yaml b/samples/community/templates/templates/goal_item.yaml index 838540b756..c04b39a99d 100644 --- a/samples/community/templates/templates/goal_item.yaml +++ b/samples/community/templates/templates/goal_item.yaml @@ -45,12 +45,13 @@ layout: align: center children: - component: Text - text: "Priority: ${priority}" + text: "Priority: {{ priority }}" variant: caption - component: Icon name: star - component: Text - text: ${title} + text: "{{ title }}" + variant: h4 - component: Divider axis: horizontal @@ -59,7 +60,7 @@ layout: align: center children: - component: Text - text: "Due: ${targetDate}" + text: "Due: {{ targetDate }}" variant: caption - component: Button child: diff --git a/samples/community/templates/templates/salary_card.yaml b/samples/community/templates/templates/salary_card.yaml index 4b09175453..7add7d2838 100644 --- a/samples/community/templates/templates/salary_card.yaml +++ b/samples/community/templates/templates/salary_card.yaml @@ -65,16 +65,19 @@ layout: text: Verified Compensation variant: caption - component: Text - text: ${clearanceLevel} + text: "{{ clearanceLevel }}" + variant: caption - component: Column children: - component: Text - text: ${employeeName} + text: "{{ employeeName }}" + variant: h3 id: name_txt - component: Text - text: ${role} + text: "{{ role }}" + variant: body - component: Divider axis: horizontal @@ -87,7 +90,8 @@ layout: text: Base Salary variant: caption - component: Text - text: ${baseSalary} + text: "{{ baseSalary }}" + variant: h4 id: sal_val - component: Column @@ -96,7 +100,8 @@ layout: text: Annual Bonus variant: caption - component: Text - text: ${annualBonus} + text: "{{ annualBonus }}" + variant: h4 - component: Column children: @@ -104,7 +109,8 @@ layout: text: Equity Grants variant: caption - component: Text - text: ${equity} + text: "{{ equity }}" + variant: h4 - component: Divider axis: horizontal @@ -116,7 +122,7 @@ layout: text: "\U0001F512 Fetched live from secure HR database" variant: caption - component: Text - text: "Verified: ${verifiedAt}" + text: "Verified: {{ verifiedAt }}" variant: caption sampleData: employeeName: Dr. Elena Vance diff --git a/samples/community/templates/templates/section_card.yaml b/samples/community/templates/templates/section_card.yaml index c6b7fd911b..6083919f7a 100644 --- a/samples/community/templates/templates/section_card.yaml +++ b/samples/community/templates/templates/section_card.yaml @@ -45,19 +45,23 @@ layout: align: center children: - component: Text - text: ${title} + text: "{{ title }}" + variant: h3 - component: Column id: action_slot - children: ${headerAction} + children: "{{ headerAction }}" + - component: Text - text: ${description} + text: "{{ description }}" + variant: caption - component: Divider axis: horizontal - component: Column id: body_container - children: ${children} + children: "{{ children }}" + sampleData: title: Protocol Overview description: High-level summary of the streaming architecture. diff --git a/samples/community/templates/templates/team_card.yaml b/samples/community/templates/templates/team_card.yaml index a2414c6da8..31811857a0 100644 --- a/samples/community/templates/templates/team_card.yaml +++ b/samples/community/templates/templates/team_card.yaml @@ -54,7 +54,8 @@ layout: align: center children: - component: Text - text: ${teamName} + text: "{{ teamName }}" + variant: h3 - component: Icon name: person diff --git a/samples/community/templates/templates/team_feedback_board.yaml b/samples/community/templates/templates/team_feedback_board.yaml index cd7bc4216d..75e4b5d6ca 100644 --- a/samples/community/templates/templates/team_feedback_board.yaml +++ b/samples/community/templates/templates/team_feedback_board.yaml @@ -56,7 +56,7 @@ layout: - component: Icon name: mail - component: Text - text: "Feedback & Retrospective: ${teamName}" + text: "Feedback & Retrospective: {{ teamName }}" variant: h2 - component: Column id: feedbacks_container diff --git a/samples/community/templates/templates/team_goal_list.yaml b/samples/community/templates/templates/team_goal_list.yaml index 3821c0aa0c..dc652ce577 100644 --- a/samples/community/templates/templates/team_goal_list.yaml +++ b/samples/community/templates/templates/team_goal_list.yaml @@ -57,7 +57,7 @@ layout: - component: Icon name: star - component: Text - text: "Strategic Objectives: ${teamName}" + text: "Strategic Objectives: {{ teamName }}" variant: h2 - component: Column id: goals_container diff --git a/samples/community/templates/templates/team_member_knowledge_panel.yaml b/samples/community/templates/templates/team_member_knowledge_panel.yaml index 1fec29bc46..bbc4ca7e1f 100644 --- a/samples/community/templates/templates/team_member_knowledge_panel.yaml +++ b/samples/community/templates/templates/team_member_knowledge_panel.yaml @@ -46,7 +46,7 @@ layout: - component: Icon name: check - component: Text - text: "Competency: ${userName}" + text: "Competency: {{ userName }}" variant: h4 - component: Divider axis: horizontal @@ -60,7 +60,8 @@ layout: text: Role variant: caption - component: Text - text: ${role} + text: "{{ role }}" + variant: body - component: Column align: center @@ -69,7 +70,8 @@ layout: text: Experience variant: caption - component: Text - text: ${experienceYears} Yrs + text: "{{ experienceYears }} Yrs" + variant: body - component: Column align: center @@ -78,7 +80,8 @@ layout: text: Tasks variant: caption - component: Text - text: ${completedTasks} Done + text: "{{ completedTasks }} Done" + variant: body - component: Text text: Verified Core Contributor diff --git a/samples/community/templates/templates/team_roster.yaml b/samples/community/templates/templates/team_roster.yaml index a41689f5c0..3bf023c3a3 100644 --- a/samples/community/templates/templates/team_roster.yaml +++ b/samples/community/templates/templates/team_roster.yaml @@ -29,12 +29,14 @@ layout: component: Column children: - component: Text - text: ${directoryTitle} + text: "{{ directoryTitle }}" + variant: h1 - component: Divider axis: horizontal - component: Column - children: ${children} + children: "{{ children }}" + sampleData: directoryTitle: Global Engineering Directory children: [] diff --git a/samples/community/templates/templates/two_column_layout.yaml b/samples/community/templates/templates/two_column_layout.yaml index 19e744501c..45aa31700f 100644 --- a/samples/community/templates/templates/two_column_layout.yaml +++ b/samples/community/templates/templates/two_column_layout.yaml @@ -35,15 +35,18 @@ layout: component: Column children: - component: Column - children: ${headerChild} + children: "{{ headerChild }}" + - component: Divider axis: horizontal - component: Row children: - component: Column - children: ${leftChildren} + children: "{{ leftChildren }}" + - component: Column - children: ${rightChildren} + children: "{{ rightChildren }}" + sampleData: headerChild: component: Text diff --git a/samples/community/templates/templates/user_profile.yaml b/samples/community/templates/templates/user_profile.yaml index 87c2bb406b..038c47f65c 100644 --- a/samples/community/templates/templates/user_profile.yaml +++ b/samples/community/templates/templates/user_profile.yaml @@ -38,10 +38,12 @@ layout: - component: Icon name: person - component: Text - text: ${userName} + text: "{{ userName }}" + variant: h3 - component: Text - text: ${role} + text: "{{ role }}" + variant: caption sampleData: userId: usr_101 From 4e1b1af2ce408d39f8398f504e9630ad349f8e5d Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Tue, 25 Aug 2026 13:12:44 +0930 Subject: [PATCH 22/26] docs(spec): add proposals for client-expanded templates and Python fluent builders --- .../templates/client_expanded_templates.md | 434 ++++++++++++++++++ .../python_fluent_template_builders.md | 357 ++++++++++++++ 2 files changed, 791 insertions(+) create mode 100644 specification/proposals/templates/client_expanded_templates.md create mode 100644 specification/proposals/templates/python_fluent_template_builders.md diff --git a/specification/proposals/templates/client_expanded_templates.md b/specification/proposals/templates/client_expanded_templates.md new file mode 100644 index 0000000000..1a5b264732 --- /dev/null +++ b/specification/proposals/templates/client_expanded_templates.md @@ -0,0 +1,434 @@ +# Proposal: Client-Expanded Templates & Dynamic MCP Template Federation + +## Abstract + +This proposal extends the A2UI Templates architecture to support **client-side template expansion** and **dynamic template federation over the Model Context Protocol (MCP)**. + +While server-expanded templates expand into canonical Basic Catalog primitives prior to leaving the Agent SDK, **client-expanded templates** are transported as compact JSON definitions over the wire. Templates can be bundled directly in `createSurface` or dynamically streamed via a new `updateTemplates` protocol message. Furthermore, components inside `updateComponents` can explicitly reference remote templates via a `templateSource` property (analogous to component-level `catalogId` in A2UI v1.0). When an unknown template source is encountered, the client framework dynamically fetches the template from the designated MCP server or endpoint, caches it locally, and synchronously inflates the layout tree on the client. + +This architecture enables: +1. **Extreme Wire Efficiency**: Transmitting compact parameter payloads over low-bandwidth channels instead of extensive repetitive component graphs. +2. **Decoupled Multi-Agent & Tool UI Federation**: Independent MCP servers (e.g., GitHub, Jira, Salesforce) can publish and maintain their own specialized UI templates without requiring the core conversational agent to know their visual layout details. +3. **Reactive Edge Expansion**: Instant client-side re-inflation when local data models mutate, without server roundtrips. + +--- + +## 1. Motivation & Problem Statement + +### The Multi-Agent & MCP UI Dilemma +In modern AI agent ecosystems, a central orchestrating agent often communicates with multiple specialized tools and subagents via the Model Context Protocol (MCP). + +Under the existing model: +- **Approach A (Server-expanded templates)**: The orchestrator's backend must hold all template layouts in memory. If a GitHub MCP server wants to display a Pull Request Card, the orchestrator needs to know the exact YAML layout of that card. This breaks tool encapsulation. +- **Approach B (Raw Basic Catalog components from tools)**: The MCP tool returns hundreds of raw UI primitives in its tool output. This significantly inflates token usage and wire payload size. + +### The Solution: Client-Expanded Federated Templates +Client-expanded templates solve this dilemma: +- The MCP server serves as the **Template Authority**, emitting specialized template definitions via `updateTemplates`. +- The central orchestrator outputs only a compact invocation: + ```json + { + "id": "pr_102", + "template": "PullRequestCard", + "templateSource": "mcp://github-tool/templates", + "parameters": { + "prNumber": 102, + "repo": "jacobsimionato/a2ui" + } + } + ``` +- The client-side A2UI framework resolves `templateSource`, loads the template definition (if not already cached), and expands the layout directly on the client. + +--- + +## 2. Protocol Message Extensions + +To support client-side templates, the A2UI message protocol is extended in two areas: `createSurface` and the new `updateTemplates` message. + +### 2.1 Bundling in `createSurface` + +Initial templates known at surface creation time can be transported directly inside `createSurface`. + +```json +{ + "version": "v1.0", + "createSurface": { + "surfaceId": "main", + "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json", + "templates": [ + { + "templateId": "MetricCard", + "description": "KPI metric card", + "parameters": { + "label": {"type": "string"}, + "value": {"type": "string"} + }, + "layout": { + "component": "Card", + "child": { + "component": "Column", + "children": [ + {"component": "Text", "text": "${label}", "variant": "caption"}, + {"component": "Text", "text": "${value}", "variant": "h2"} + ] + } + } + } + ] + } +} +``` + +### 2.2 The `updateTemplates` Message + +For templates delivered after surface creation, or emitted dynamically by connected MCP servers, the protocol introduces the top-level `updateTemplates` message. + +#### Schema Specification: +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI UpdateTemplates Message", + "type": "object", + "required": ["templates"], + "properties": { + "surfaceId": { + "type": "string", + "description": "Optional surface to which these templates are scoped. If omitted, templates are scoped globally to the client session." + }, + "templateSource": { + "type": "string", + "description": "The authority URI or MCP server identifier publishing these templates (e.g. 'mcp://github-server/templates')." + }, + "replace": { + "type": "boolean", + "default": false, + "description": "If true, clears previously registered templates under this templateSource before adding the new set." + }, + "templates": { + "type": "array", + "items": { + "$ref": "template_definition.json" + }, + "description": "Array of JSON-serialized template definitions." + } + } +} +``` + +#### Example Message: +```json +{ + "version": "v1.0", + "updateTemplates": { + "templateSource": "mcp://github-service/templates", + "templates": [ + { + "templateId": "PullRequestCard", + "description": "GitHub PR status card with merge status and CI indicator.", + "parameters": { + "prNumber": {"type": "integer"}, + "title": {"type": "string"}, + "author": {"type": "string"}, + "status": {"type": "enum", "values": ["open", "merged", "closed"]} + }, + "layout": { + "component": "Card", + "child": { + "component": "Column", + "children": [ + { + "component": "Row", + "justify": "spaceBetween", + "children": [ + {"component": "Text", "text": "#${prNumber} ${title}", "variant": "h3"}, + {"component": "Text", "text": "${status}", "variant": "caption"} + ] + }, + {"component": "Divider", "axis": "horizontal"}, + {"component": "Text", "text": "Opened by @${author}", "variant": "body"} + ] + } + } + } + ] + } +} +``` + +--- + +## 3. Template Referencing in A2UI Content + +### 3.1 Component-Level Referencing & Contrast with v1.0 `catalogId` + +In the A2UI v1.0 specification, `ComponentCommon` introduced `catalogId` on individual components to override the surface-level default catalog: + +```json +// A2UI v1.0 Component with custom catalogId +{ + "id": "chart_1", + "catalogId": "https://company.org/catalogs/analytics/catalog.json", + "component": "LineChart", + "series": "/metrics/cpu" +} +``` + +Analogously, client-expanded templates introduce `template` and `templateSource` to the component model: + +```json +// Client-Expanded Template Component Reference +{ + "id": "pr_widget_1", + "template": "PullRequestCard", + "templateSource": "mcp://github-service/templates", + "parameters": { + "prNumber": 342, + "title": "Add client-side template expansion", + "author": "octocat", + "status": "open" + } +} +``` + +#### Property Definitions: +- **`template`** (string, required): The ID of the template to instantiate (e.g. `"PullRequestCard"`). +- **`templateSource`** (string, optional): The URI or namespace of the template provider. + - Can be an MCP URI: `mcp:///` + - Can be an HTTPS endpoint: `https://templates.a2ui.org/library/v1` + - Can be a local identifier: `local` or `shared` + - If omitted, the template is resolved from the surface's default template registry. +- **`parameters`** (object, required): A key-value mapping matching the template's declared parameter schema. + +--- + +## 4. MCP Server Integration & Dynamic Federation + +When a client encounters a template invocation with a `templateSource` that has not yet been loaded, it uses the Model Context Protocol (MCP) to fetch the template on demand. + +### 4.1 Sequence Diagram: On-Demand Dynamic Resolution + +```mermaid +sequenceDiagram + autonumber + participant Agent as Conversational Agent + participant MCP as MCP Tool Server (e.g. GitHub) + participant Client as A2UI Client Framework + participant UI as User Interface + + Note over Agent, Client: User asks: "Show me PR #342" + Agent->>MCP: Call tool: get_pull_request(342) + MCP-->>Agent: Returns PR data {prNumber: 342, title: "Add client templates", ...} + + Agent->>Client: updateComponents(components: [{id: "card_1", template: "PullRequestCard", templateSource: "mcp://github-server/templates", parameters: {...}}]) + + Note over Client: Client checks local template cache for "PullRequestCard" from "mcp://github-server/templates"... (MISS) + + Client->>UI: Render Skeleton / Placeholder for "card_1" + + Client->>MCP: Request MCP Resource or Tool: get_template("PullRequestCard") + MCP-->>Client: updateTemplates(templateSource: "mcp://github-server/templates", templates: [PullRequestCard]) + + Note over Client: Client caches PullRequestCard
Expands template locally with parameters
Injects synthetic child components + + Client->>UI: Smoothly replaces Skeleton with rendered PullRequestCard! +``` + +### 4.2 Proactive vs. Reactive MCP Template Delivery + +The architecture supports both delivery patterns: + +1. **Proactive Delivery (Tool Response Multiplexing)**: + When an MCP server returns a tool response to an agent, it can include an `updateTemplates` message in its response payload or stream it directly over the client channel. When the client receives the agent's `updateComponents` message, the template is already warm in memory. + +2. **Reactive Delivery (Client On-Demand Fetching)**: + If an agent references a template that the client has not yet received: + - The client invokes the standard MCP resource: + `mcp:///templates/` + - The MCP server returns the template JSON in an `updateTemplates` payload. + +--- + +## 5. Client Expansion Engine & Lifecycle + +### 5.1 Deterministic Client Synthetic ID Generation +When the client-side A2UI framework expands a template, it preserves the existing adjacency list component architecture. It generates synthetic IDs for all internal components of the template layout: + +$$\text{syntheticId} = \{\text{hostId}\} \mathbin{\_} \{\text{slotName}\} \mathbin{\_} \{\text{index}\} \mathbin{\_} \{\text{componentType}\}$$ + +#### Example Expansion: +Given host component: +```json +{ + "id": "team_lead_widget", + "template": "UserProfile", + "parameters": {"userName": "Elena Vance", "role": "Principal"} +} +``` + +The client engine replaces `team_lead_widget` in its active render tree with: +```json +[ + { + "id": "team_lead_widget", + "component": "Card", + "child": "team_lead_widget_child_column" + }, + { + "id": "team_lead_widget_child_column", + "component": "Column", + "children": [ + "team_lead_widget_child_column_children_0_text", + "team_lead_widget_child_column_children_1_text" + ] + }, + { + "id": "team_lead_widget_child_column_children_0_text", + "component": "Text", + "text": "Elena Vance", + "variant": "h3" + }, + { + "id": "team_lead_widget_child_column_children_1_text", + "component": "Text", + "text": "Principal", + "variant": "caption" + } +] +``` + +### 5.2 Progressive Rendering & Skeleton Loading +Because remote template retrieval involves an asynchronous network hop, the client framework must provide a seamless user experience: +1. **Placeholder State**: When an un-cached template is encountered, the client mounts a canonical placeholder (e.g. an animated skeleton card with the specified dimensions or basic text). +2. **Progressive Mounting**: Once `updateTemplates` is processed, the placeholder is smoothly replaced with the expanded component tree without unmounting neighboring components. +3. **Timeout Fallback**: If the template source fails to resolve within a configurable timeout (default: 3000ms), a clean error card is displayed: + `"Unable to load template 'PullRequestCard' from mcp://github-server/templates"`. + +### 5.3 Two-Way Client Data Binding Interoperability +Client-expanded templates have direct, low-latency access to the client's `DataModel`: +- **Local Interpolation**: If a template parameter contains a data binding path (e.g. `path: "/currentUser/name"`), the client resolves it reactively against the local surface data store. +- **Dynamic Updates**: When the agent sends `updateDataModel`, any client-expanded template bound to that path automatically updates without needing re-expansion. + +--- + +## 6. End-to-End Concrete Example + +### Step 1: MCP Server Emits `updateTemplates` +```json +{ + "version": "v1.0", + "updateTemplates": { + "templateSource": "mcp://cloud-ops/templates", + "templates": [ + { + "templateId": "ServerHealthCard", + "parameters": { + "hostname": {"type": "string"}, + "cpuUsage": {"type": "number"}, + "status": {"type": "enum", "values": ["healthy", "warning", "down"]} + }, + "layout": { + "component": "Card", + "child": { + "component": "Column", + "children": [ + { + "component": "Row", + "justify": "spaceBetween", + "children": [ + {"component": "Text", "text": "${hostname}", "variant": "h3"}, + {"component": "Icon", "name": "dns"} + ] + }, + {"component": "Divider", "axis": "horizontal"}, + {"component": "Text", "text": "CPU Load: ${cpuUsage}%", "variant": "body"} + ] + } + } + } + ] + } +} +``` + +### Step 2: Agent Sends Compact `updateComponents` +```json +{ + "version": "v1.0", + "updateComponents": { + "surfaceId": "ops_dashboard", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["server_1", "server_2"] + }, + { + "id": "server_1", + "template": "ServerHealthCard", + "templateSource": "mcp://cloud-ops/templates", + "parameters": { + "hostname": "prod-api-01", + "cpuUsage": 42.5, + "status": "healthy" + } + }, + { + "id": "server_2", + "template": "ServerHealthCard", + "templateSource": "mcp://cloud-ops/templates", + "parameters": { + "hostname": "prod-db-01", + "cpuUsage": 89.1, + "status": "warning" + } + } + ] + } +} +``` + +### Step 3: Client Inflates Component Subtrees Locally +The client expands `server_1` and `server_2` into native `Card`, `Column`, `Row`, and `Text` nodes, rendering the complete dashboard with zero server expansion overhead. + +--- + +## 7. Edge Cases & Boundary Conditions + +### 1. Template Namespace Collisions +**Problem**: Two distinct MCP servers (e.g. `mcp://github` and `mcp://gitlab`) both define a template named `IssueCard`. +**Solution**: Templates are keyed internally in the client cache by the composite tuple `(templateSource, templateId)`. If `templateSource` is omitted in the invocation, the resolution order is: +1. Active surface local templates. +2. Global session templates. +3. Fallback error if multiple matching templates exist in different sources. + +### 2. Security & Declarative Sandboxing +**Risk**: Can an untrusted MCP server deliver malicious executable scripts inside a template? +**Mitigation**: +- A2UI templates are strictly **declarative JSON data structures** validating against `template_definition.json`. +- Templates contain *no executable code*, JavaScript, or eval functions. +- The client engine validates all component types within a template against the surface's allowed `catalogId`. A template cannot invoke unverified or unauthorized custom components. + +### 3. Client Recursion Limits +To prevent cyclic template references (e.g. Template `A` invokes Template `B` which invokes Template `A`), the client expansion engine maintains a call-stack set and enforces a strict depth limit: +```typescript +const MAX_CLIENT_TEMPLATE_DEPTH = 16; +``` +If this depth is exceeded, the client halts expansion and marks the node as `TemplateRecursionError`. + +### 4. Cache Persistence & Invalidation +- **Session Cache**: Cached in memory for the duration of the client connection. +- **Persistent Cache (Optional)**: If the client enables local storage caching (e.g. IndexedDB), templates with versioned sources (e.g. `mcp://github/templates/v1.2.0`) can be cached across browser refreshes, making subsequent loads instant. +- **Invalidation**: Sending an `updateTemplates` message with `replace: true` flushes the cache for that `templateSource`. + +--- + +## 8. Summary Comparison: Server vs. Client Expansion + +| Feature | Server-Expanded Templates | Client-Expanded Templates | +| :--- | :--- | :--- | +| **Expansion Location** | Agent SDK (Server-Side) | A2UI Framework / Renderer (Client-Side) | +| **Wire Protocol Payload** | Standard Basic Catalog primitives (`Card`, `Text`, etc.) | Compact JSON template references & `updateTemplates` | +| **Client Requirement** | Zero. Any standard Basic Catalog renderer works. | Client must include template inflation logic (`@a2ui/templates`). | +| **Dynamic Tool Federation** | Central orchestrator must hold all tool templates. | Autonomous MCP servers publish templates dynamically via `updateTemplates`. | +| **Confidential Data Resolution** | Excellent (secure server callbacks query private DBs). | Moderate (private data must be transmitted to the client). | +| **Wire Bandwidth** | Higher (expanded component graphs sent over wire). | Minimal (only parameters and IDs sent over wire). | diff --git a/specification/proposals/templates/python_fluent_template_builders.md b/specification/proposals/templates/python_fluent_template_builders.md new file mode 100644 index 0000000000..5495c732d8 --- /dev/null +++ b/specification/proposals/templates/python_fluent_template_builders.md @@ -0,0 +1,357 @@ +# Proposal: Python Fluent Template Builders + +## Abstract + +This document proposes a catalog-driven code generator and type-safe fluent builder library for authoring A2UI templates and dynamic server render functions in Python. + +By analyzing an A2UI Component Catalog JSON Schema (`catalog.json`), a builder generator script automatically synthesizes strongly typed, IDE-friendly Python component classes (`Card`, `Column`, `Row`, `Text`, `Button`, `Divider`, `Icon`, `Image`, etc.). These builder classes enforce compile-time and runtime type safety, provide instant editor autocompletion, and seamlessly interoperate with the A2UI Python Agent SDK's polymorphic template engine. + +--- + +## 1. Motivation + +The A2UI Python SDK supports programmatic dynamic templates via server render functions: + +```python +def render_team_dashboard(team_id: str) -> dict: + ... +``` + +While authoring UI trees using raw Python dictionaries is flexible, it exposes developers to common mistakes: +- **Typo errors**: Accidentally typing `"algn": "center"` instead of `"align": "center"`. +- **Type mismatches**: Passing an integer `42` where a child component or string is expected. +- **Invalid enum variants**: Passing `variant="heading1"` instead of standard `variant="h1"`. +- **Lack of discoverability**: Developers must consult documentation or JSON schemas to know what properties a component accepts. + +A catalog-driven fluent builder library solves these issues by turning JSON schemas into native Python classes with type annotations, docstrings, and constructor validation. + +--- + +## 2. Architecture & Code Generation Workflow + +The builder system is catalog-driven and catalog-agnostic: + +``` ++-------------------------------------------------------------------------------+ +| A2UI Catalog JSON Schema (e.g. basic/catalog.json or custom_catalog.json) | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| Generator CLI Tool: a2ui-build-builders | +| 1. Ingests catalog schema (local file path or remote HTTPS URL) | +| 2. Parses component definitions, slots, properties, and enum constraints | +| 3. Emits type-annotated builder dataclasses with docstrings | +| 4. Formats output code with pyink / black | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| Output Python Package: a2ui.inference_formats.experimental.template.builder | +| - __init__.py | +| - components.py (Card, Column, Row, Text, Button, Icon, Divider, Image...) | +| - types.py (TextVariant, FlexAlign, FlexJustify, etc.) | ++---------------------------------------+---------------------------------------+ + | + v ++-------------------------------------------------------------------------------+ +| Programmatic DynamicTemplate (render=render_fn) | +| Python functions return fluent builder trees with IDE autocompletion | ++-------------------------------------------------------------------------------+ +``` + +--- + +## 3. Package Architecture: Generated vs. Handcrafted Files + +To maintain clean separation between runtime engine logic and generated code, the builder architecture splits into handcrafted runtime infrastructure and auto-generated catalog modules: + +``` +agent_sdks/python/ +├── a2ui_agent/ +│ ├── src/a2ui/inference_formats/experimental/template/ +│ │ ├── builder/ <-- Pre-bundled builders for Basic Catalog +│ │ │ ├── __init__.py (Generated / Re-exported) +│ │ │ ├── base.py (Handcrafted Runtime Infrastructure) +│ │ │ ├── components.py (Auto-Generated from basic/catalog.json) +│ │ │ └── types.py (Auto-Generated from basic/catalog.json) +│ │ ├── models.py (Handcrafted: normalize_node, DynamicTemplate) +│ │ └── processor.py (Handcrafted: TemplateProcessor) +│ └── tools/ +│ └── codegen/ +│ ├── __init__.py +│ └── builder_generator.py (Handcrafted: The Codegen CLI Tool) +``` + +### A. Handcrafted Runtime Infrastructure (Static SDK Code, Never Regenerated) +These files provide the foundation and serialization contracts: + +1. **`base.py` (`ComponentBuilder`, `DataBinding`)**: + - `ComponentBuilder`: The abstract base class providing the `.to_dict()` protocol and equality checking. + - `DataBinding`: Strongly typed wrapper for client-side reactive bindings (`path: str`). + - `ActionRef`: Helper for dispatching user interaction events. +2. **`models.py` (`normalize_node`, `flatten_nested_layout`)**: + - Handles duck-typing normalization. If a node implements `.to_dict()` (like `ComponentBuilder`), it automatically serializes it before flattening synthetic IDs. +3. **`processor.py` (`TemplateProcessor`)**: + - The synchronous expansion engine that resolves dynamic functions and flattens layouts into standard Basic Catalog primitives. + +### B. Auto-Generated Files (Synthesized from `catalog.json`) +These files are generated on demand from any A2UI catalog schema: + +1. **`components.py`**: + - Python `@dataclass` classes for every component in the catalog (`Text`, `Card`, `Column`, `Row`, `Button`, `Divider`, `Icon`, `Image`, etc.). + - Explicit constructor parameters (no untyped `**kwargs`). + - Python docstrings extracted directly from the `description` fields in the catalog schema. + - Implementation of `.to_dict()` that recursively normalizes child slots and data bindings. +2. **`types.py`**: + - Strict `Literal[...]` unions for all string enums defined in the catalog (e.g. `TextVariant`, `FlexAlign`, `FlexJustify`, `ButtonVariant`). +3. **`__init__.py`**: + - Re-exports all generated component classes alongside `DataBinding` and `ComponentBuilder` for clean imports (`from a2ui.inference_formats.experimental.template.builder import Card, Column, Text, DataBinding`). + +--- + +## 4. Code Generation CLI & Distribution + +### 4.1 CLI Entry Point Registration + +The code generator is distributed as part of `a2ui-agent-sdk` (and standalone via `a2ui-core`). It is registered as a console script in `pyproject.toml`: + +```toml +[project.scripts] +a2ui-build-builders = "a2ui.tools.codegen.builder_generator:main" +``` + +### 4.2 Pre-bundled Builders (Zero-Setup for Standard Users) +For standard A2UI development using the official **Basic Catalog**, developers do not need to run any code generation steps. The `a2ui-agent-sdk` package ships with pre-generated, tested builders directly under `a2ui.inference_formats.experimental.template.builder`. + +Developers simply write: +```python +from a2ui.inference_formats.experimental.template.builder import Card, Column, Text, Icon, Divider, DataBinding +``` + +### 4.3 Generating Builders for Custom / Enterprise Catalogs +When an enterprise team defines a custom component catalog (e.g. `finance_catalog.json` with `StockTicker`, `OrderBook`, `CandlestickChart`), they can run the CLI generator to produce their own custom builder package: + +```bash +# Run via uv in any project +uv run a2ui-build-builders \ + --catalog ./catalogs/finance/catalog.json \ + --output ./src/my_app/ui_builders \ + --package-name my_app.ui_builders +``` + +#### CLI Options & Flags: + +| Flag | Type | Description | +| :--- | :--- | :--- | +| `--catalog`, `-c` | `str` (Required) | Path to local `catalog.json` or remote HTTPS schema URL. | +| `--output`, `-o` | `str` (Required) | Target directory where Python builder files will be written. | +| `--package-name` | `str` (Optional) | Custom Python package namespace for generated imports. | +| `--format` | `bool` (Default: `True`) | Automatically format generated code using `pyink` or `black`. | +| `--strict-types` | `bool` (Default: `True`) | Emit `Literal` enum types and strict primitive unions. | + +### 4.4 Hatchling / Build Hook Integration +In the A2UI repository, the builder generation is wired into the build lifecycle (`pack_specs_hook.py` or `wireit`). When specifications under `specification/v0_9_1/catalogs/` update, the build hook regenerates `a2ui.template.builder` automatically before wheels are built. + +--- + +## 5. Type Mapping Rules + +The generator maps JSON schema types to Python type annotations: + +| A2UI Catalog JSON Type | Generated Python Type Annotation | Description | +| :--- | :--- | :--- | +| `DynamicString` | `Union[str, DataBinding]` | Literal string or client data model binding. | +| `DynamicNumber` | `Union[int, float, DataBinding]` | Numeric literal or client data model binding. | +| `DynamicBoolean` | `Union[bool, DataBinding]` | Boolean flag or client data model binding. | +| `ComponentId` / `child` | `Union[ComponentBuilder, Dict[str, Any], str]` | Single child component, raw dict AST, or ID reference. | +| `ChildList` / `children` | `Union[List[Union[ComponentBuilder, Dict[str, Any], str]], Dict[str, Any]]` | Multi-child list or inline loop definition. | +| `Action` | `Union[ActionRef, Dict[str, Any], str]` | Action event definition or action ID. | +| `enum` (e.g. `variant`) | `Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"]` | Strict string literals for IDE auto-complete. | + +--- + +## 6. Generated Class Structure Example + +Here is the exact code emitted by `a2ui-build-builders` for standard components: + +```python +# a2ui/inference_formats/experimental/template/builder/components.py +# AUTO-GENERATED BY a2ui-build-builders. DO NOT EDIT DIRECTLY. + +from __future__ import annotations +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional, Union +from a2ui.inference_formats.experimental.template.models import normalize_node +from .base import ComponentBuilder, DataBinding, ActionRef +from .types import TextVariant, FlexAlign, FlexJustify, DividerAxis + +@dataclass +class Text(ComponentBuilder): + """Text component for structured typographic display. + + Parameters: + text: The text content to display, or a DataBinding path. + variant: Typographic hierarchy level ('h1' through 'h5', 'caption', 'body'). + id: Optional explicit component identifier. + """ + text: Union[str, DataBinding] + variant: Optional[TextVariant] = "body" + id: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = { + "component": "Text", + "text": self.text.to_dict() if isinstance(self.text, DataBinding) else self.text, + } + if self.variant: + res["variant"] = self.variant + if self.id: + res["id"] = self.id + return res + + +@dataclass +class Column(ComponentBuilder): + """Vertical layout container. + + Parameters: + children: List of child component builders or raw AST dicts. + align: Cross-axis alignment ('start', 'center', 'end', 'stretch'). + justify: Main-axis distribution ('start', 'center', 'end', 'spaceBetween', 'spaceAround'). + id: Optional explicit component identifier. + """ + children: Union[List[Union[ComponentBuilder, Dict[str, Any], str]], Dict[str, Any]] + align: Optional[FlexAlign] = None + justify: Optional[FlexJustify] = None + id: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = {"component": "Column"} + if isinstance(self.children, list): + res["children"] = [normalize_node(c) for c in self.children] + else: + res["children"] = normalize_node(self.children) + if self.align: + res["align"] = self.align + if self.justify: + res["justify"] = self.justify + if self.id: + res["id"] = self.id + return res + + +@dataclass +class Card(ComponentBuilder): + """Visual surface container card. + + Parameters: + child: Single child component contained within the card surface. + id: Optional explicit component identifier. + """ + child: Union[ComponentBuilder, Dict[str, Any], str] + id: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + res: Dict[str, Any] = { + "component": "Card", + "child": normalize_node(self.child), + } + if self.id: + res["id"] = self.id + return res +``` + +--- + +## 7. Developer Experience: Programmatic Dynamic Templates + +With fluent builders, developers author server render functions with complete IDE autocompletion, type safety, and inline docstrings: + +```python +from a2ui.inference_formats.experimental.template.builder import Card, Column, Row, Text, Icon, Divider, DataBinding +from a2ui.inference_formats.experimental.template import DynamicTemplate + +def render_project_status(project_id: str): + project = project_db.get(project_id) + + # 1. Native Python conditionals + status_icon = "check_circle" if project.status == "ON_TRACK" else "warning" + + # 2. Native Python list comprehensions + milestone_rows = [ + Row( + justify="spaceBetween", + children=[ + Text(m.name, variant="body"), + Text(f"Due: {m.due_date}", variant="caption"), + ] + ) + for m in project.milestones + ] + + # 3. Reactive two-way client DataBinding + approval_binding = DataBinding(path=f"/projects/{project_id}/budgetApproved") + + # 4. Construct type-safe component tree + return Card( + child=Column( + children=[ + Row( + justify="spaceBetween", + align="center", + children=[ + Text(project.name, variant="h2"), + Icon(name=status_icon), + ] + ), + Divider(), + Text(f"Lead Architect: {project.lead_name}", variant="caption"), + Text(f"Sprint Velocity: {project.velocity} pts", variant="body"), + Divider(), + Text("Milestones", variant="h3"), + Column(children=milestone_rows), + ] + ) + ) + +# Register DynamicTemplate +project_template = DynamicTemplate( + template_id="ProjectStatusCard", + render=render_project_status, + description="Live project tracking card with milestones.", +) +``` + +--- + +## 8. Static Type Checking & IDE Feedback + +When developers write code using fluent builders, IDEs (VS Code / Pylance, PyCharm, Cursor) and static type checkers (`mypy`, `pyright`) catch errors before runtime: + +### 1. Typo Prevention +```python +# Mypy Error: Unexpected keyword argument "algn" for "Column" +Column(children=[], algn="center") +``` + +### 2. Slot Type Validation +```python +# Mypy Error: Argument "child" to "Card" has incompatible type "int"; expected "ComponentBuilder | dict | str" +Card(child=123) +``` + +### 3. Enum Variant Checking +```python +# Mypy Error: Argument "variant" to "Text" has incompatible type 'Literal["heading1"]'; expected 'Literal["h1", "h2", ...]' +Text("Title", variant="heading1") +``` + +--- + +## 9. Polymorphic Engine Interoperability + +Because the engine's [`flatten_nested_layout()`](file:///Users/jsimionato/development/a2ui_repos/templates/a2ui/agent_sdks/python/a2ui_agent/src/a2ui/template/models.py#L358) includes duck-type normalization (`hasattr(node, "to_dict")`), fluent builder trees: +- Are flattened automatically into canonical Basic Catalog primitives with synthetic IDs (`root`, `{parent}_{slot}_{index}_{type}`). +- Can be freely mixed with raw dictionaries, dataclasses, or Pydantic models anywhere in the hierarchy. +- Require zero modifications to `TemplateProcessor` or `TemplateInferenceFormat`. From a45dbdc657864b56afb9ce598b7b76c5b76cc4f0 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 11:00:13 +0930 Subject: [PATCH 23/26] style(templates): format future proposal markdown documents with prettier --- .../templates/client_expanded_templates.md | 63 +++++++++++++------ .../python_fluent_template_builders.md | 51 +++++++++------ 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/specification/proposals/templates/client_expanded_templates.md b/specification/proposals/templates/client_expanded_templates.md index 1a5b264732..dc2c2a7dac 100644 --- a/specification/proposals/templates/client_expanded_templates.md +++ b/specification/proposals/templates/client_expanded_templates.md @@ -2,11 +2,12 @@ ## Abstract -This proposal extends the A2UI Templates architecture to support **client-side template expansion** and **dynamic template federation over the Model Context Protocol (MCP)**. +This proposal extends the A2UI Templates architecture to support **client-side template expansion** and **dynamic template federation over the Model Context Protocol (MCP)**. While server-expanded templates expand into canonical Basic Catalog primitives prior to leaving the Agent SDK, **client-expanded templates** are transported as compact JSON definitions over the wire. Templates can be bundled directly in `createSurface` or dynamically streamed via a new `updateTemplates` protocol message. Furthermore, components inside `updateComponents` can explicitly reference remote templates via a `templateSource` property (analogous to component-level `catalogId` in A2UI v1.0). When an unknown template source is encountered, the client framework dynamically fetches the template from the designated MCP server or endpoint, caches it locally, and synchronously inflates the layout tree on the client. This architecture enables: + 1. **Extreme Wire Efficiency**: Transmitting compact parameter payloads over low-bandwidth channels instead of extensive repetitive component graphs. 2. **Decoupled Multi-Agent & Tool UI Federation**: Independent MCP servers (e.g., GitHub, Jira, Salesforce) can publish and maintain their own specialized UI templates without requiring the core conversational agent to know their visual layout details. 3. **Reactive Edge Expansion**: Instant client-side re-inflation when local data models mutate, without server roundtrips. @@ -16,14 +17,18 @@ This architecture enables: ## 1. Motivation & Problem Statement ### The Multi-Agent & MCP UI Dilemma -In modern AI agent ecosystems, a central orchestrating agent often communicates with multiple specialized tools and subagents via the Model Context Protocol (MCP). + +In modern AI agent ecosystems, a central orchestrating agent often communicates with multiple specialized tools and subagents via the Model Context Protocol (MCP). Under the existing model: + - **Approach A (Server-expanded templates)**: The orchestrator's backend must hold all template layouts in memory. If a GitHub MCP server wants to display a Pull Request Card, the orchestrator needs to know the exact YAML layout of that card. This breaks tool encapsulation. - **Approach B (Raw Basic Catalog components from tools)**: The MCP tool returns hundreds of raw UI primitives in its tool output. This significantly inflates token usage and wire payload size. ### The Solution: Client-Expanded Federated Templates + Client-expanded templates solve this dilemma: + - The MCP server serves as the **Template Authority**, emitting specialized template definitions via `updateTemplates`. - The central orchestrator outputs only a compact invocation: ```json @@ -84,6 +89,7 @@ Initial templates known at surface creation time can be transported directly ins For templates delivered after surface creation, or emitted dynamically by connected MCP servers, the protocol introduces the top-level `updateTemplates` message. #### Schema Specification: + ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -116,6 +122,7 @@ For templates delivered after surface creation, or emitted dynamically by connec ``` #### Example Message: + ```json { "version": "v1.0", @@ -191,6 +198,7 @@ Analogously, client-expanded templates introduce `template` and `templateSource` ``` #### Property Definitions: + - **`template`** (string, required): The ID of the template to instantiate (e.g. `"PullRequestCard"`). - **`templateSource`** (string, optional): The URI or namespace of the template provider. - Can be an MCP URI: `mcp:///` @@ -218,18 +226,18 @@ sequenceDiagram Note over Agent, Client: User asks: "Show me PR #342" Agent->>MCP: Call tool: get_pull_request(342) MCP-->>Agent: Returns PR data {prNumber: 342, title: "Add client templates", ...} - + Agent->>Client: updateComponents(components: [{id: "card_1", template: "PullRequestCard", templateSource: "mcp://github-server/templates", parameters: {...}}]) - + Note over Client: Client checks local template cache for "PullRequestCard" from "mcp://github-server/templates"... (MISS) - + Client->>UI: Render Skeleton / Placeholder for "card_1" - + Client->>MCP: Request MCP Resource or Tool: get_template("PullRequestCard") MCP-->>Client: updateTemplates(templateSource: "mcp://github-server/templates", templates: [PullRequestCard]) - + Note over Client: Client caches PullRequestCard
Expands template locally with parameters
Injects synthetic child components - + Client->>UI: Smoothly replaces Skeleton with rendered PullRequestCard! ``` @@ -251,12 +259,15 @@ The architecture supports both delivery patterns: ## 5. Client Expansion Engine & Lifecycle ### 5.1 Deterministic Client Synthetic ID Generation + When the client-side A2UI framework expands a template, it preserves the existing adjacency list component architecture. It generates synthetic IDs for all internal components of the template layout: $$\text{syntheticId} = \{\text{hostId}\} \mathbin{\_} \{\text{slotName}\} \mathbin{\_} \{\text{index}\} \mathbin{\_} \{\text{componentType}\}$$ #### Example Expansion: + Given host component: + ```json { "id": "team_lead_widget", @@ -266,6 +277,7 @@ Given host component: ``` The client engine replaces `team_lead_widget` in its active render tree with: + ```json [ { @@ -297,14 +309,18 @@ The client engine replaces `team_lead_widget` in its active render tree with: ``` ### 5.2 Progressive Rendering & Skeleton Loading + Because remote template retrieval involves an asynchronous network hop, the client framework must provide a seamless user experience: + 1. **Placeholder State**: When an un-cached template is encountered, the client mounts a canonical placeholder (e.g. an animated skeleton card with the specified dimensions or basic text). 2. **Progressive Mounting**: Once `updateTemplates` is processed, the placeholder is smoothly replaced with the expanded component tree without unmounting neighboring components. 3. **Timeout Fallback**: If the template source fails to resolve within a configurable timeout (default: 3000ms), a clean error card is displayed: `"Unable to load template 'PullRequestCard' from mcp://github-server/templates"`. ### 5.3 Two-Way Client Data Binding Interoperability + Client-expanded templates have direct, low-latency access to the client's `DataModel`: + - **Local Interpolation**: If a template parameter contains a data binding path (e.g. `path: "/currentUser/name"`), the client resolves it reactively against the local surface data store. - **Dynamic Updates**: When the agent sends `updateDataModel`, any client-expanded template bound to that path automatically updates without needing re-expansion. @@ -313,6 +329,7 @@ Client-expanded templates have direct, low-latency access to the client's `DataM ## 6. End-to-End Concrete Example ### Step 1: MCP Server Emits `updateTemplates` + ```json { "version": "v1.0", @@ -351,6 +368,7 @@ Client-expanded templates have direct, low-latency access to the client's `DataM ``` ### Step 2: Agent Sends Compact `updateComponents` + ```json { "version": "v1.0", @@ -388,6 +406,7 @@ Client-expanded templates have direct, low-latency access to the client's `DataM ``` ### Step 3: Client Inflates Component Subtrees Locally + The client expands `server_1` and `server_2` into native `Card`, `Column`, `Row`, and `Text` nodes, rendering the complete dashboard with zero server expansion overhead. --- @@ -395,27 +414,35 @@ The client expands `server_1` and `server_2` into native `Card`, `Column`, `Row` ## 7. Edge Cases & Boundary Conditions ### 1. Template Namespace Collisions + **Problem**: Two distinct MCP servers (e.g. `mcp://github` and `mcp://gitlab`) both define a template named `IssueCard`. **Solution**: Templates are keyed internally in the client cache by the composite tuple `(templateSource, templateId)`. If `templateSource` is omitted in the invocation, the resolution order is: + 1. Active surface local templates. 2. Global session templates. 3. Fallback error if multiple matching templates exist in different sources. ### 2. Security & Declarative Sandboxing + **Risk**: Can an untrusted MCP server deliver malicious executable scripts inside a template? -**Mitigation**: +**Mitigation**: + - A2UI templates are strictly **declarative JSON data structures** validating against `template_definition.json`. -- Templates contain *no executable code*, JavaScript, or eval functions. +- Templates contain _no executable code_, JavaScript, or eval functions. - The client engine validates all component types within a template against the surface's allowed `catalogId`. A template cannot invoke unverified or unauthorized custom components. ### 3. Client Recursion Limits + To prevent cyclic template references (e.g. Template `A` invokes Template `B` which invokes Template `A`), the client expansion engine maintains a call-stack set and enforces a strict depth limit: + ```typescript const MAX_CLIENT_TEMPLATE_DEPTH = 16; ``` + If this depth is exceeded, the client halts expansion and marks the node as `TemplateRecursionError`. ### 4. Cache Persistence & Invalidation + - **Session Cache**: Cached in memory for the duration of the client connection. - **Persistent Cache (Optional)**: If the client enables local storage caching (e.g. IndexedDB), templates with versioned sources (e.g. `mcp://github/templates/v1.2.0`) can be cached across browser refreshes, making subsequent loads instant. - **Invalidation**: Sending an `updateTemplates` message with `replace: true` flushes the cache for that `templateSource`. @@ -424,11 +451,11 @@ If this depth is exceeded, the client halts expansion and marks the node as `Tem ## 8. Summary Comparison: Server vs. Client Expansion -| Feature | Server-Expanded Templates | Client-Expanded Templates | -| :--- | :--- | :--- | -| **Expansion Location** | Agent SDK (Server-Side) | A2UI Framework / Renderer (Client-Side) | -| **Wire Protocol Payload** | Standard Basic Catalog primitives (`Card`, `Text`, etc.) | Compact JSON template references & `updateTemplates` | -| **Client Requirement** | Zero. Any standard Basic Catalog renderer works. | Client must include template inflation logic (`@a2ui/templates`). | -| **Dynamic Tool Federation** | Central orchestrator must hold all tool templates. | Autonomous MCP servers publish templates dynamically via `updateTemplates`. | -| **Confidential Data Resolution** | Excellent (secure server callbacks query private DBs). | Moderate (private data must be transmitted to the client). | -| **Wire Bandwidth** | Higher (expanded component graphs sent over wire). | Minimal (only parameters and IDs sent over wire). | +| Feature | Server-Expanded Templates | Client-Expanded Templates | +| :------------------------------- | :------------------------------------------------------- | :-------------------------------------------------------------------------- | +| **Expansion Location** | Agent SDK (Server-Side) | A2UI Framework / Renderer (Client-Side) | +| **Wire Protocol Payload** | Standard Basic Catalog primitives (`Card`, `Text`, etc.) | Compact JSON template references & `updateTemplates` | +| **Client Requirement** | Zero. Any standard Basic Catalog renderer works. | Client must include template inflation logic (`@a2ui/templates`). | +| **Dynamic Tool Federation** | Central orchestrator must hold all tool templates. | Autonomous MCP servers publish templates dynamically via `updateTemplates`. | +| **Confidential Data Resolution** | Excellent (secure server callbacks query private DBs). | Moderate (private data must be transmitted to the client). | +| **Wire Bandwidth** | Higher (expanded component graphs sent over wire). | Minimal (only parameters and IDs sent over wire). | diff --git a/specification/proposals/templates/python_fluent_template_builders.md b/specification/proposals/templates/python_fluent_template_builders.md index 5495c732d8..bdd6e5f08d 100644 --- a/specification/proposals/templates/python_fluent_template_builders.md +++ b/specification/proposals/templates/python_fluent_template_builders.md @@ -2,7 +2,7 @@ ## Abstract -This document proposes a catalog-driven code generator and type-safe fluent builder library for authoring A2UI templates and dynamic server render functions in Python. +This document proposes a catalog-driven code generator and type-safe fluent builder library for authoring A2UI templates and dynamic server render functions in Python. By analyzing an A2UI Component Catalog JSON Schema (`catalog.json`), a builder generator script automatically synthesizes strongly typed, IDE-friendly Python component classes (`Card`, `Column`, `Row`, `Text`, `Button`, `Divider`, `Icon`, `Image`, etc.). These builder classes enforce compile-time and runtime type safety, provide instant editor autocompletion, and seamlessly interoperate with the A2UI Python Agent SDK's polymorphic template engine. @@ -18,6 +18,7 @@ def render_team_dashboard(team_id: str) -> dict: ``` While authoring UI trees using raw Python dictionaries is flexible, it exposes developers to common mistakes: + - **Typo errors**: Accidentally typing `"algn": "center"` instead of `"align": "center"`. - **Type mismatches**: Passing an integer `42` where a child component or string is expected. - **Invalid enum variants**: Passing `variant="heading1"` instead of standard `variant="h1"`. @@ -84,6 +85,7 @@ agent_sdks/python/ ``` ### A. Handcrafted Runtime Infrastructure (Static SDK Code, Never Regenerated) + These files provide the foundation and serialization contracts: 1. **`base.py` (`ComponentBuilder`, `DataBinding`)**: @@ -96,6 +98,7 @@ These files provide the foundation and serialization contracts: - The synchronous expansion engine that resolves dynamic functions and flattens layouts into standard Basic Catalog primitives. ### B. Auto-Generated Files (Synthesized from `catalog.json`) + These files are generated on demand from any A2UI catalog schema: 1. **`components.py`**: @@ -122,14 +125,17 @@ a2ui-build-builders = "a2ui.tools.codegen.builder_generator:main" ``` ### 4.2 Pre-bundled Builders (Zero-Setup for Standard Users) + For standard A2UI development using the official **Basic Catalog**, developers do not need to run any code generation steps. The `a2ui-agent-sdk` package ships with pre-generated, tested builders directly under `a2ui.inference_formats.experimental.template.builder`. Developers simply write: + ```python from a2ui.inference_formats.experimental.template.builder import Card, Column, Text, Icon, Divider, DataBinding ``` ### 4.3 Generating Builders for Custom / Enterprise Catalogs + When an enterprise team defines a custom component catalog (e.g. `finance_catalog.json` with `StockTicker`, `OrderBook`, `CandlestickChart`), they can run the CLI generator to produce their own custom builder package: ```bash @@ -142,15 +148,16 @@ uv run a2ui-build-builders \ #### CLI Options & Flags: -| Flag | Type | Description | -| :--- | :--- | :--- | -| `--catalog`, `-c` | `str` (Required) | Path to local `catalog.json` or remote HTTPS schema URL. | -| `--output`, `-o` | `str` (Required) | Target directory where Python builder files will be written. | -| `--package-name` | `str` (Optional) | Custom Python package namespace for generated imports. | -| `--format` | `bool` (Default: `True`) | Automatically format generated code using `pyink` or `black`. | -| `--strict-types` | `bool` (Default: `True`) | Emit `Literal` enum types and strict primitive unions. | +| Flag | Type | Description | +| :---------------- | :----------------------- | :------------------------------------------------------------ | +| `--catalog`, `-c` | `str` (Required) | Path to local `catalog.json` or remote HTTPS schema URL. | +| `--output`, `-o` | `str` (Required) | Target directory where Python builder files will be written. | +| `--package-name` | `str` (Optional) | Custom Python package namespace for generated imports. | +| `--format` | `bool` (Default: `True`) | Automatically format generated code using `pyink` or `black`. | +| `--strict-types` | `bool` (Default: `True`) | Emit `Literal` enum types and strict primitive unions. | ### 4.4 Hatchling / Build Hook Integration + In the A2UI repository, the builder generation is wired into the build lifecycle (`pack_specs_hook.py` or `wireit`). When specifications under `specification/v0_9_1/catalogs/` update, the build hook regenerates `a2ui.template.builder` automatically before wheels are built. --- @@ -159,15 +166,15 @@ In the A2UI repository, the builder generation is wired into the build lifecycle The generator maps JSON schema types to Python type annotations: -| A2UI Catalog JSON Type | Generated Python Type Annotation | Description | -| :--- | :--- | :--- | -| `DynamicString` | `Union[str, DataBinding]` | Literal string or client data model binding. | -| `DynamicNumber` | `Union[int, float, DataBinding]` | Numeric literal or client data model binding. | -| `DynamicBoolean` | `Union[bool, DataBinding]` | Boolean flag or client data model binding. | -| `ComponentId` / `child` | `Union[ComponentBuilder, Dict[str, Any], str]` | Single child component, raw dict AST, or ID reference. | -| `ChildList` / `children` | `Union[List[Union[ComponentBuilder, Dict[str, Any], str]], Dict[str, Any]]` | Multi-child list or inline loop definition. | -| `Action` | `Union[ActionRef, Dict[str, Any], str]` | Action event definition or action ID. | -| `enum` (e.g. `variant`) | `Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"]` | Strict string literals for IDE auto-complete. | +| A2UI Catalog JSON Type | Generated Python Type Annotation | Description | +| :----------------------- | :-------------------------------------------------------------------------- | :----------------------------------------------------- | +| `DynamicString` | `Union[str, DataBinding]` | Literal string or client data model binding. | +| `DynamicNumber` | `Union[int, float, DataBinding]` | Numeric literal or client data model binding. | +| `DynamicBoolean` | `Union[bool, DataBinding]` | Boolean flag or client data model binding. | +| `ComponentId` / `child` | `Union[ComponentBuilder, Dict[str, Any], str]` | Single child component, raw dict AST, or ID reference. | +| `ChildList` / `children` | `Union[List[Union[ComponentBuilder, Dict[str, Any], str]], Dict[str, Any]]` | Multi-child list or inline loop definition. | +| `Action` | `Union[ActionRef, Dict[str, Any], str]` | Action event definition or action ID. | +| `enum` (e.g. `variant`) | `Literal["h1", "h2", "h3", "h4", "h5", "caption", "body"]` | Strict string literals for IDE auto-complete. | --- @@ -189,7 +196,7 @@ from .types import TextVariant, FlexAlign, FlexJustify, DividerAxis @dataclass class Text(ComponentBuilder): """Text component for structured typographic display. - + Parameters: text: The text content to display, or a DataBinding path. variant: Typographic hierarchy level ('h1' through 'h5', 'caption', 'body'). @@ -214,7 +221,7 @@ class Text(ComponentBuilder): @dataclass class Column(ComponentBuilder): """Vertical layout container. - + Parameters: children: List of child component builders or raw AST dicts. align: Cross-axis alignment ('start', 'center', 'end', 'stretch'). @@ -244,7 +251,7 @@ class Column(ComponentBuilder): @dataclass class Card(ComponentBuilder): """Visual surface container card. - + Parameters: child: Single child component contained within the card surface. id: Optional explicit component identifier. @@ -330,18 +337,21 @@ project_template = DynamicTemplate( When developers write code using fluent builders, IDEs (VS Code / Pylance, PyCharm, Cursor) and static type checkers (`mypy`, `pyright`) catch errors before runtime: ### 1. Typo Prevention + ```python # Mypy Error: Unexpected keyword argument "algn" for "Column" Column(children=[], algn="center") ``` ### 2. Slot Type Validation + ```python # Mypy Error: Argument "child" to "Card" has incompatible type "int"; expected "ComponentBuilder | dict | str" Card(child=123) ``` ### 3. Enum Variant Checking + ```python # Mypy Error: Argument "variant" to "Text" has incompatible type 'Literal["heading1"]'; expected 'Literal["h1", "h2", ...]' Text("Title", variant="heading1") @@ -352,6 +362,7 @@ Text("Title", variant="heading1") ## 9. Polymorphic Engine Interoperability Because the engine's [`flatten_nested_layout()`](file:///Users/jsimionato/development/a2ui_repos/templates/a2ui/agent_sdks/python/a2ui_agent/src/a2ui/template/models.py#L358) includes duck-type normalization (`hasattr(node, "to_dict")`), fluent builder trees: + - Are flattened automatically into canonical Basic Catalog primitives with synthetic IDs (`root`, `{parent}_{slot}_{index}_{type}`). - Can be freely mixed with raw dictionaries, dataclasses, or Pydantic models anywhere in the hierarchy. - Require zero modifications to `TemplateProcessor` or `TemplateInferenceFormat`. From e3a2f3c6c76cbf95c5cf28b43e388a2ddf255b49 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:25:31 +0930 Subject: [PATCH 24/26] docs(templates): align future proposals with name, catalogs, and imports --- .../proposals/templates/client_expanded_templates.md | 9 ++++++--- .../templates/python_fluent_template_builders.md | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/specification/proposals/templates/client_expanded_templates.md b/specification/proposals/templates/client_expanded_templates.md index dc2c2a7dac..51654ff6d7 100644 --- a/specification/proposals/templates/client_expanded_templates.md +++ b/specification/proposals/templates/client_expanded_templates.md @@ -62,7 +62,8 @@ Initial templates known at surface creation time can be transported directly ins "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json", "templates": [ { - "templateId": "MetricCard", + "name": "MetricCard", + "catalogs": ["https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"], "description": "KPI metric card", "parameters": { "label": {"type": "string"}, @@ -130,7 +131,8 @@ For templates delivered after surface creation, or emitted dynamically by connec "templateSource": "mcp://github-service/templates", "templates": [ { - "templateId": "PullRequestCard", + "name": "PullRequestCard", + "catalogs": ["https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"], "description": "GitHub PR status card with merge status and CI indicator.", "parameters": { "prNumber": {"type": "integer"}, @@ -337,7 +339,8 @@ Client-expanded templates have direct, low-latency access to the client's `DataM "templateSource": "mcp://cloud-ops/templates", "templates": [ { - "templateId": "ServerHealthCard", + "name": "ServerHealthCard", + "catalogs": ["https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"], "parameters": { "hostname": {"type": "string"}, "cpuUsage": {"type": "number"}, diff --git a/specification/proposals/templates/python_fluent_template_builders.md b/specification/proposals/templates/python_fluent_template_builders.md index bdd6e5f08d..dd84ac0b17 100644 --- a/specification/proposals/templates/python_fluent_template_builders.md +++ b/specification/proposals/templates/python_fluent_template_builders.md @@ -324,7 +324,9 @@ def render_project_status(project_id: str): # Register DynamicTemplate project_template = DynamicTemplate( + name="ProjectStatusCard", template_id="ProjectStatusCard", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], render=render_project_status, description="Live project tracking card with milestones.", ) From eec5d35998eb564300af23210e32e3d6817eec75 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 12:36:22 +0930 Subject: [PATCH 25/26] docs(templates): use @dynamic_template decorator in fluent builder proposal --- .../python_fluent_template_builders.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/specification/proposals/templates/python_fluent_template_builders.md b/specification/proposals/templates/python_fluent_template_builders.md index dd84ac0b17..f966867d88 100644 --- a/specification/proposals/templates/python_fluent_template_builders.md +++ b/specification/proposals/templates/python_fluent_template_builders.md @@ -277,9 +277,14 @@ With fluent builders, developers author server render functions with complete ID ```python from a2ui.inference_formats.experimental.template.builder import Card, Column, Row, Text, Icon, Divider, DataBinding -from a2ui.inference_formats.experimental.template import DynamicTemplate +from a2ui.inference_formats.experimental.template import dynamic_template -def render_project_status(project_id: str): +@dynamic_template( + name="ProjectStatusCard", + catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], + description="Live project tracking card with milestones.", +) +def project_status_card(project_id: str): project = project_db.get(project_id) # 1. Native Python conditionals @@ -321,15 +326,6 @@ def render_project_status(project_id: str): ] ) ) - -# Register DynamicTemplate -project_template = DynamicTemplate( - name="ProjectStatusCard", - template_id="ProjectStatusCard", - catalogs=["https://a2ui.org/specification/v0_9_1/catalogs/basic/catalog.json"], - render=render_project_status, - description="Live project tracking card with milestones.", -) ``` --- From 9de65d1739365943da038fd2aeb8f2d1d2338b84 Mon Sep 17 00:00:00 2001 From: Jacob Simionato Date: Wed, 26 Aug 2026 13:32:28 +0930 Subject: [PATCH 26/26] docs(spec): adopt double-curly Mustache syntax in client-expanded templates proposal --- .../templates/client_expanded_templates.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/specification/proposals/templates/client_expanded_templates.md b/specification/proposals/templates/client_expanded_templates.md index 51654ff6d7..7f1c8c9ef7 100644 --- a/specification/proposals/templates/client_expanded_templates.md +++ b/specification/proposals/templates/client_expanded_templates.md @@ -74,8 +74,8 @@ Initial templates known at surface creation time can be transported directly ins "child": { "component": "Column", "children": [ - {"component": "Text", "text": "${label}", "variant": "caption"}, - {"component": "Text", "text": "${value}", "variant": "h2"} + {"component": "Text", "text": "{{ label }}", "variant": "caption"}, + {"component": "Text", "text": "{{ value }}", "variant": "h2"} ] } } @@ -149,12 +149,12 @@ For templates delivered after surface creation, or emitted dynamically by connec "component": "Row", "justify": "spaceBetween", "children": [ - {"component": "Text", "text": "#${prNumber} ${title}", "variant": "h3"}, - {"component": "Text", "text": "${status}", "variant": "caption"} + {"component": "Text", "text": "#{{ prNumber }} {{ title }}", "variant": "h3"}, + {"component": "Text", "text": "{{ status }}", "variant": "caption"} ] }, {"component": "Divider", "axis": "horizontal"}, - {"component": "Text", "text": "Opened by @${author}", "variant": "body"} + {"component": "Text", "text": "Opened by @{{ author }}", "variant": "body"} ] } } @@ -355,12 +355,12 @@ Client-expanded templates have direct, low-latency access to the client's `DataM "component": "Row", "justify": "spaceBetween", "children": [ - {"component": "Text", "text": "${hostname}", "variant": "h3"}, + {"component": "Text", "text": "{{ hostname }}", "variant": "h3"}, {"component": "Icon", "name": "dns"} ] }, {"component": "Divider", "axis": "horizontal"}, - {"component": "Text", "text": "CPU Load: ${cpuUsage}%", "variant": "body"} + {"component": "Text", "text": "CPU Load: {{ cpuUsage }}%", "variant": "body"} ] } }