diff --git a/.agent/skills/genui-helper/SKILL.md b/.agent/skills/genui-helper/SKILL.md new file mode 100644 index 000000000..d74154b53 --- /dev/null +++ b/.agent/skills/genui-helper/SKILL.md @@ -0,0 +1,63 @@ +--- +name: genui-helper +description: > + Development helper for the GenUI repository. Use this skill when the user asks + about GenUI workflows, running tests, creating components, finding A2UI or + Dart references, or adhering to repository standards. +--- + +# GenUI Development Helper + +This skill provides workflows and best practices specific to the `genui` repository. + +## Workflows + +### 1. Running Tests and Fixes + +The repository uses a custom tool to run tests, apply fixes, and format code before committing. +It should typically only be run before committing, since it is inefficient and slow to run it on every change. + +It will run `dart fix --apply`, `dart format`, and `flutter test` on all packages in the repository. + +**Command:** +```bash +dart run tool/test_and_fix/bin/test_and_fix.dart +``` + +**When to use:** +- Before committing changes, to ensure project health. +- Instead of running `flutter test` manually for each project in the repo. + +### 2. Creating a New Component in the genui package + +When creating a new UI component in `genui`: + +1. **Location**: Place component files in `packages/genui/lib/src/components/`. +2. **Inheritance**: Components must extend `UiComponent`. +3. **A2UI Compliance**: Ensure the component matches the A2UI specification. +4. **Documentation**: Follow strict Dart documentation standards. + +### 3. Updating Documentation + +- Documentation source of truth is in `docs/`. +- Use `mkdocs` context if mentioned, but primarily edit the markdown files directly. +- Ensure strict adherence to "Natural Writing" standards (no AI-isms). + +## Key Constants & Patterns + +- **Current A2UI Version**: v0.9 +- **State Management**: Uses `SurfaceController` from `genui`. + +## References + +- A2UI Specification + - Available in the submodule at @packages/genui/submodules/a2ui + - The specification documentation is available in @packages/genui/submodules/a2ui/specification/v0.9/docs + - The specification schemas are available in @packages/genui/submodules/a2ui/specification/v0.9/json + - Because it is a submodule, you may need to update the submodule to get the latest specification. +- To find out details of a specific dart compiler diagnostic message, use the following url format to look up the details: + - https://dart.dev/tools/diagnostics/ + - Example: https://dart.dev/tools/diagnostics/ambiguous_import +- To find out details of a specific analyzer lint message, use the following url format to look up the details: + - https://dart.dev/tools/linter-rules/ + - Example: https://dart.dev/tools/linter-rules/always_declare_return_types \ No newline at end of file diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 7a009e1a5..a8f6fefa1 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -1,3 +1,5 @@ # Gemini Code Assistant Context Follow the specifications in `specs/README.md`. + +You can find additional skills in @.agent/skills \ No newline at end of file diff --git a/.github/workflows/flutter_packages.yaml b/.github/workflows/flutter_packages.yaml index 03599485d..6e75b69af 100644 --- a/.github/workflows/flutter_packages.yaml +++ b/.github/workflows/flutter_packages.yaml @@ -54,8 +54,14 @@ jobs: # Get the list of changed files. The method depends on the event type. if [[ "${{ github.event_name }}" == "pull_request" ]]; then - # For PRs, use the GitHub CLI to get a precise list of changed files. - CHANGED_FILES=$(gh pr diff --name-only ${{ github.event.pull_request.number }}) + # Try using gh pr diff first to get the precise list of changed files. + # This avoids false positives if main has moved forward. + # We wrap it in an if statement to catch failures (e.g. diff too large). + if ! CHANGED_FILES=$(gh pr diff --name-only ${{ github.event.pull_request.number }}); then + echo "Warning: 'gh pr diff' failed. This usually happens when the PR is very large." + echo "Falling back to 'git diff' against origin/${{ github.base_ref }}." + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} HEAD) + fi else # For pushes, diff between the two SHAs of the push. CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.event.after }}) diff --git a/.gitmodules b/.gitmodules index 0b7d298ab..8d7b0c972 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "packages/json_schema_builder/submodules/JSON-Schema-Test-Suite"] path = packages/json_schema_builder/submodules/JSON-Schema-Test-Suite - url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git \ No newline at end of file + url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git +[submodule "a2ui"] + path = packages/genui/submodules/a2ui + url = https://github.com/google/A2UI.git + branch = main diff --git a/README.md b/README.md index 7054b35a3..7455b4bac 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,9 @@ based on a widget catalog from the developers' project. ## Connecting to an AI agent -The `genui` framework uses a `ContentGenerator` to communicate with a generative AI model, -allowing `genui` to be backend agnostic. You can choose the implementation that best fits -your needs, whether it's `FirebaseAiContentGenerator` for production apps, -`GoogleGenerativeAiContentGenerator` for rapid prototyping, or `A2uiContentGenerator` for -custom agent servers. +The `genui` framework is designed to be backend agnostic. You can use any AI SDK (such as `google_generative_ai`, `dartantic_ai`, or `firebase_vertexai`) to generate content. The framework provides adapters (like `A2uiTransportAdapter`) to ingest the AI response and render it. + +For custom agent servers that implement the A2UI protocol, you can use the `genui_a2ui` package. See the package table below for more details on each. @@ -97,10 +95,7 @@ See the package table below for more details on each. | Package | Description | Version | | :--- | :--- | :--- | | [genui](packages/genui/) | The core framework to employ Generative UI. | [![pub package](https://img.shields.io/pub/v/genui.svg)](https://pub.dev/packages/genui) | -| [genui_firebase_ai](packages/genui_firebase_ai/) | Provides **`FirebaseAiContentGenerator`** to connect to Gemini via Firebase AI Logic. This is the recommended approach for production apps based on client-side agents. | [![pub package](https://img.shields.io/pub/v/genui_firebase_ai.svg)](https://pub.dev/packages/genui_firebase_ai) | -| [genui_google_generative_ai](packages/genui_google_generative_ai/) | Provides **`GoogleGenerativeAiContentGenerator`** for connecting to the Google Generative AI API with only an API key. Ideal for getting started quickly. | [![pub package](https://img.shields.io/pub/v/genui_google_generative_ai.svg)](https://pub.dev/packages/genui_google_generative_ai) | -| [genui_a2ui](packages/genui_a2ui/) | Provides **`A2uiContentGenerator`** for connecting to any server that implements the [A2UI protocol](https://a2ui.org). Use this for integrating with custom agent backends. | [![pub package](https://img.shields.io/pub/v/genui_a2ui.svg)](https://pub.dev/packages/genui_a2ui) | -| [genui_dartantic](packages/genui_dartantic/) | Integration package for genui and Dartantic AI. | [![pub package](https://img.shields.io/pub/v/genui_dartantic.svg)](https://pub.dev/packages/genui_dartantic) | +| [genui_a2ui](packages/genui_a2ui/) | Provides **`A2uiAgentConnector`** for connecting to any server that implements the [A2UI protocol](https://a2ui.org). Use this for integrating with custom agent backends. | [![pub package](https://img.shields.io/pub/v/genui_a2ui.svg)](https://pub.dev/packages/genui_a2ui) | | [genai_primitives](packages/genai_primitives/) | A set of technology-agnostic primitive types and data structures for building Generative AI applications. | [![pub package](https://img.shields.io/pub/v/genai_primitives.svg)](https://pub.dev/packages/genai_primitives) | | [json_schema_builder](packages/json_schema_builder/) | A fully featured Dart JSON Schema package with validation, used by the core framework to define widget data structures. | [![pub package](https://img.shields.io/pub/v/json_schema_builder.svg)](https://pub.dev/packages/json_schema_builder) | @@ -110,24 +105,19 @@ This diagram shows how packages depend on each other and how examples use them. ```mermaid graph TD - examples/simple_chat --> genui_google_generative_ai - examples/simple_chat --> genui_firebase_ai - examples/travel_app --> genui_google_generative_ai - examples/travel_app --> genui_firebase_ai + examples/catalog_gallery --> genui + examples/simple_chat --> genui + examples/travel_app --> genui examples/verdure --> genui_a2ui - examples/custom_backend --> genui genui --> json_schema_builder genui_a2ui --> genui - genui_dartantic --> genui - genui_firebase_ai --> genui - genui_google_generative_ai --> genui ``` ## A2UI Support The Flutter Gen UI SDK uses the [A2UI protocol](https://a2ui.org) to represent UI content internally. The [genui_a2ui](packages/genui_a2ui/) package allows it to act as a renderer for UIs generated by an A2UI backend agent, similar to the [other A2UI renderers](https://github.com/google/A2UI/tree/main/renderers) which are maintained within the A2UI repository. -The Flutter Gen UI SDK currently supports A2UI v0.8. +The Flutter Gen UI SDK currently supports A2UI v0.9. ## Getting started diff --git a/analysis_options.yaml b/analysis_options.yaml index 281a71bf8..e9ea616de 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,6 +13,7 @@ analyzer: linter: rules: # consistency + - avoid_print - combinators_ordering - directives_ordering - lines_longer_than_80_chars diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 000000000..a74d3ff8c --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,78 @@ +# `genui` Package Implementation + +This document provides a comprehensive overview of the architecture, purpose, and implementation of the `genui` package. + +## Purpose + +The `genui` package provides the core framework for building Flutter applications with dynamically generated user interfaces powered by large language models (LLMs). It enables developers to create conversational UIs where the interface is not static or predefined, but is instead constructed by an AI in real-time based on the user's prompts and the flow of the conversation. + +The package supplies the essential components for managing the state of the dynamic UI, interacting with the AI model, defining a vocabulary of UI widgets, and rendering the UI surfaces. + +## Architecture + +The package is designed with a layered architecture, separating concerns to create a flexible and extensible framework. The diagram below shows how the `genui` package integrates with the developer's application and the backend LLM. + +![Class Diagram](./assets/class-diagram.svg) + +### 1. Transport Layer (`lib/src/transport/` and `lib/src/interfaces/`) + +This layer handles the pipeline from raw text input (from an LLM) to parsed UI events. + +- **`Transport`**: An interface defining the contract for sending and receiving messages. +- **`A2uiTransportAdapter`**: The default implementation of `Transport`. It manages the input stream (`addChunk`), the parsing pipeline, and communicates with the `Conversation`. It uses the `A2uiParserTransformer` to parse streams. +- **`A2uiParserTransformer`**: A robust stream transformer that parses mixed streams of text and A2UI JSON messages. It handles buffering, validation, and conversion of raw strings into structured `GenerationEvent`s. + +### 2. UI State Management Layer (`lib/src/engine/`) + +This is the central nervous system of the package, orchestrating the state of all generated UI surfaces. + +- **`SurfaceController`**: The core state manager for the dynamic UI. It maintains a map of all active UI "surfaces", where each surface is represented by a `UiDefinition`. The AI interacts with the manager by sending structured A2UI messages, which the controller handles via `handleMessage()`. It exposes a stream of `SurfaceUpdate` events (`SurfaceAdded`, `ComponentsUpdated`, `SurfaceRemoved`) so that the application can react to changes. It also owns the `DataModel` to manage the state of individual widgets and implements `SurfaceHost` to provide `SurfaceContext`s for `Surface` widgets. + +### 3. UI Model Layer (`lib/src/model/`) + +This layer defines the data structures that represent the dynamic UI and the conversation. + +- **`Catalog` and `CatalogItem`**: These classes define the registry of available UI components. The `Catalog` holds a list of `CatalogItem`s, and each `CatalogItem` defines a widget's name, its data schema, and a builder function to render it. +- **`A2uiMessage`**: A sealed class (`lib/src/model/a2ui_message.dart`) representing the commands the AI sends to the UI. It has the following subtypes: + - `CreateSurface`: Signals the start of rendering for a surface. + - `UpdateComponents`: Adds or updates components on a surface. + - `UpdateDataModel`: Modifies data within the `DataModel` for a surface. + - `DeleteSurface`: Requests the removal of a surface. + The schemas for these messages are defined in `lib/src/model/a2ui_schemas.dart`. +- **`UiDefinition` and `UiEvent`**: `UiDefinition` represents the state of a surface, including the definitions of all components on the surface. `UiEvent` is a data object representing a user interaction. `UserActionEvent` is a subtype used for events that should trigger a submission to the AI, like a button tap. +- **`ChatMessage`**: A sealed class representing the different types of messages in a conversation: `UserMessage`, `AiTextMessage`, `ToolResponseMessage`, `AiUiMessage`, `InternalMessage`, and `UserUiInteractionMessage`. +- **`DataModel` and `DataContext`**: The `DataModel` is a centralized, observable key-value store that holds the entire dynamic state of the UI. Widgets receive a `DataContext`, which is a view into the `DataModel` that understands the widget's current scope. This allows widgets to subscribe to changes in the data model and rebuild reactively. This separation of data and UI structure is a core principle of the architecture. + +### 4. Widget Catalog Layer (`lib/src/catalog/`) + +This layer provides a set of core, general-purpose UI widgets that can be used out-of-the-box. + +- **`basic_catalog.dart`**: Defines the `BasicCatalogItems`, which includes fundamental widgets like `AudioPlayer`, `Button`, `Card`, `CheckBox`, `Column`, `DateTimeInput`, `Divider`, `Icon`, `Image`, `List`, `Modal`, `MultipleChoice`, `Row`, `Slider`, `Tabs`, `Text`, `TextField`, and `Video`. +- **Widget Implementation**: Each core widget follows the standard `CatalogItem` pattern: a schema definition, a type-safe data accessor using an `extension type`, the `CatalogItem` instance, and the Flutter widget implementation. + +### 5. UI Facade Layer (`lib/src/conversation/`) + +This layer provides high-level widgets and controllers for easily building a generative UI application. + +- **`Conversation`**: The primary entry point for the package. This facade class encapsulates the `SurfaceController` (engine) and the `Transport`. It manages the conversation loop, piping messages between the transport and the engine. +- **`Surface`**: The Flutter widget responsible for recursively building a UI tree from a `UiDefinition`. It listens for updates from a `SurfaceContext` (typically obtained from a `SurfaceHost` like `SurfaceController`) and rebuilds itself when the definition changes. + +### 6. Primitives Layer (`lib/src/primitives/`) + +This layer contains basic utilities used throughout the package. + +- **`logging.dart`**: Provides a configurable logger (`genUiLogger`). +- **`simple_items.dart`**: Defines a type alias for `JsonMap`. + +### 7. Direct Call Integration (`lib/src/facade/direct_call_integration/`) + +This directory provides utilities for a more direct interaction with the AI model, potentially bypassing some of the higher-level abstractions of `Conversation`. It includes: + +- **`model.dart`**: Defines data models for direct API calls. +- **`utils.dart`**: Contains utility functions to assist with direct calls. + +## How It Works: The Generative UI Cycle + +The `Conversation` simplifies the process of creating a generative UI by managing the conversation loop and the interaction with the AI. + +![Architecture](./assets/architecture.svg) \ No newline at end of file diff --git a/docs/Design_Proposal.md b/docs/Design_Proposal.md new file mode 100644 index 000000000..6b7631c0b --- /dev/null +++ b/docs/Design_Proposal.md @@ -0,0 +1,643 @@ +# GenUI API Design: Moving to "Bring Your Own LLM" + +## Executive Summary + +The current `genui` architecture relies on a `ContentGenerator` abstraction that wraps the LLM interaction, managing both the network connection and the state of the conversation. While this provides a unified interface for the framework, it creates friction for developers who want to integrate GenUI into existing applications with established LLM pipelines (e.g., Genkit, custom loops, or other AI SDKs). + +With the shift to A2UI v0.9 and its "Prompt-First" philosophy—where the LLM streams text containing embedded JSON blocks—we have an opportunity to simplify the API. We can decouple the **content source** from the **content parsing and rendering**, allowing developers to "bring their own" LLM inference while still leveraging GenUI's powerful rendering capabilities. + +This report proposes a **Unified Architecture** to achieve this goal, combining high-level ease of use with low-level composability. + +## Current State + +Currently, `Conversation` requires a `ContentGenerator`: + +``` +abstract interface class ContentGenerator { + Stream get a2uiMessageStream; + Stream get textResponseStream; + Future sendRequest(ChatMessage message, {...}); + // ... +} +``` + +### Issues + +1. **Inversion of Control:** The framework calls `sendRequest`, forcing the developer to implement the API call inside the framework's structure. +2. **State Management Duplication:** The `ContentGenerator` often replicates state management (history, tokens) that might already exist in the developer's app. +3. **Hidden Parsing:** The logic to extract A2UI messages from the text stream is buried within specific `ContentGenerator` implementations (e.g., `GoogleGenerativeAiContentGenerator`), making it hard to reuse just the parser. + +## Design Goals + +1. **Decoupling:** Separate the *source* of the stream (LLM) from the *consumer* (UI). +2. **Flexibility:** Allow any string stream (WebSocket, local model, mock, HTTP stream) to drive the UI. +3. **Simplicity:** Reduce the boilerplate needed to start rendering A2UI content. +4. **Bi-directionality:** maintain support for client-to-server `Action`s (events) and `ToolCall`s. + +## Architecture + +The `genui` package adopts a decoupled, event-driven architecture that separates the UI presentation from the transport and state management layers. This design allows developers to bring their own LLM or backend service while leveraging GenUI's rendering capabilities. + +The following diagram illustrates the core data flow: + +![Architecture](assets/architecture.svg) + +## Class Diagram + +![Class Diagram](assets/class-diagram.svg) + +### 1. Transport Layer (`lib/src/transport/` and `lib/src/interfaces/`) + +This layer handles the pipeline from raw text input (from an LLM) to parsed UI events. + +- **`Transport`**: An interface defining the contract for sending and receiving messages. +- **`A2uiTransportAdapter`**: The default implementation of `Transport`. It manages the input stream (`addChunk`), the parsing pipeline, and communicates with the `Conversation`. It uses the `A2uiParserTransformer` to parse streams. +- **`A2uiParserTransformer`**: A robust stream transformer that parses mixed streams of text and A2UI JSON messages. It handles buffering, validation, and conversion of raw strings into structured `GenerationEvent`s. + +### 2. UI State Management Layer (`lib/src/engine/`) + +This is the central nervous system of the package, orchestrating the state of all generated UI surfaces. + +- **`SurfaceController`**: The core state manager for the dynamic UI (formerly `GenUiController`). It maintains a map of all active UI "surfaces", where each surface is represented by a `UiDefinition`. It takes a `SurfaceConfiguration` object that can restrict AI actions. The AI interacts with the manager by sending structured A2UI messages, which the controller handles via `handleMessage()`. It exposes a stream of `SurfaceUpdate` events (`SurfaceAdded`, `ComponentsUpdated`, `SurfaceRemoved`) so that the application can react to changes. It also owns the `DataModel` to manage the state of individual widgets and implements `SurfaceHost` to provide `SurfaceContext`s for `Surface` widgets. + +### 3. UI Model Layer (`lib/src/model/`) + +This layer defines the data structures that represent the dynamic UI and the conversation. + +- **`Catalog` and `CatalogItem`**: These classes define the registry of available UI components. The `Catalog` holds a list of `CatalogItem`s, and each `CatalogItem` defines a widget's name, its data schema, and a builder function to render it. +- **`A2uiMessage`**: A sealed class (`lib/src/model/a2ui_message.dart`) representing the commands the AI sends to the UI. It has the following subtypes: + - `CreateSurface`: Signals the start of rendering for a surface, specifying the root component and optionally requests the client to send the data model (`sendDataModel`). + - `UpdateComponents`: Adds or updates components on a surface. + - `UpdateDataModel`: Modifies data within the `DataModel` for a surface. + - `DeleteSurface`: Requests the removal of a surface. The schemas for these messages are defined in `lib/src/model/a2ui_schemas.dart`. +- **`UiDefinition` and `UiEvent`**: `UiDefinition` represents a complete UI tree to be rendered, including the root widget and a map of all widget definitions. `UiEvent` is a data object representing a user interaction. `UserActionEvent` is a subtype used for events that should trigger a submission to the AI, like a button tap. +- **`ChatMessage`**: A sealed class representing the different types of messages in a conversation: `UserMessage`, `AiTextMessage`, `ToolResponseMessage`, `AiUiMessage`, `InternalMessage`, and `UserUiInteractionMessage`. +- **`DataModel` and `DataContext`**: The `DataModel` is a centralized, observable key-value store that holds the entire dynamic state of the UI. Widgets receive a `DataContext`, which is a view into the `DataModel` that understands the widget's current scope. This allows widgets to subscribe to changes in the data model and rebuild reactively. This separation of data and UI structure is a core principle of the architecture. + +### 4. Widget Catalog Layer (`lib/src/catalog/`) + +This layer provides a set of core, general-purpose UI widgets that can be used out-of-the-box. + +- **`SurfaceContext`**: A scoped interface that provides access to the state and behavior of a specific UI surface. It is created by a `SurfaceHost` (like `SurfaceController`). +- **`SurfaceHost`**: The interface that manages multiple surfaces and providers `SurfaceContext`s. +- **`basic_catalog.dart`**: Defines the `BasicCatalogItems`, which includes fundamental widgets like `AudioPlayer`, `Button`, `Card`, `CheckBox`, `Column`, `DateTimeInput`, `Divider`, `Icon`, `Image`, `List`, `Modal`, `MultipleChoice`, `Row`, `Slider`, `Tabs`, `Text`, `TextField`, and `Video`. +- **Widget Implementation**: Each core widget follows the standard `CatalogItem` pattern: a schema definition, a type-safe data accessor using an `extension type`, the `CatalogItem` instance, and the Flutter widget implementation. + +### 5. UI Facade Layer (`lib/src/facade/`) + +This layer provides high-level widgets and controllers for easily building a generative UI application. + +- **`Conversation`**: The primary entry point for the package. This facade class encapsulates the `SurfaceController` and `Transport`, managing the conversation loop. It abstracts away the complexity of piping events between the transport and the engine. +- **`Surface`**: The Flutter widget responsible for recursively building a UI tree from a `UiDefinition`. It listens for updates from a `SurfaceContext` (typically obtained from a `SurfaceHost` like `SurfaceController`) and rebuilds itself when the definition changes. + +### 6. Primitives Layer (`lib/src/primitives/`) + +This layer contains basic utilities used throughout the package. + +- **`logging.dart`**: Provides a configurable logger (`genUiLogger`). +- **`simple_items.dart`**: Defines a type alias for `JsonMap`. + +### 7. Direct Call Integration (`lib/src/facade/direct_call_integration/`) + +This directory provides utilities for a more direct interaction with the AI model, potentially bypassing some of the higher-level abstractions of `Conversation`. It includes: + +- **`model.dart`**: Defines data models for direct API calls. +- **`utils.dart`**: Contains utility functions to assist with direct calls. + +## Surface Lifecycle & Cleanup + +When multiple surfaces are generated in a conversation, `SurfaceController` manages them according to a `SurfaceCleanupStrategy`. The default is `ManualCleanupStrategy` (keep all surfaces until explicitly deleted), but `KeepLastNCleanupStrategy` is common for chat interfaces where only the newest UI matters. + +### Example: `KeepLastNCleanupStrategy(1)` + +```mermaid +sequenceDiagram + participant LLM as External LLM + participant Transport as Transport + participant Controller as SurfaceController + participant UI as Surface + + Note over Controller: Strategy: KeepLastN(1) + + LLM->>Transport: "createSurface(id: 'A')" + Transport->>Controller: CreateSurface('A') + Controller->>UI: SurfaceAdded('A') + UI->>UI: Render Surface A + + LLM->>Transport: "updateComponents(id: 'A', ...)" + Transport->>Controller: UpdateComponents('A') + Controller->>UI: ComponentsUpdated('A') + UI->>UI: Update Surface A + + LLM->>Transport: "createSurface(id: 'B')" + Transport->>Controller: CreateSurface('B') + + rect rgba(255, 240, 240, 0.5) + Note right of Controller: Policy Enforcement + Controller->>UI: SurfaceRemoved('A') + UI->>UI: Dispose Surface A + end + + Controller->>UI: SurfaceAdded('B') + UI->>UI: Render Surface B +``` + + +## Detailed API Reference + +### Core & Entry Points + +These classes form the backbone of the GenUI integration in your app. + +#### `lib/genui.dart` + +**Purpose:** The main entry point for the package. Exports all public APIs. +**Used For:** Import this file to access all GenUI classes. +**Code Example:** + +``` +import 'package:genui/genui.dart'; +``` + +#### `lib/src/transport/a2ui_transport_adapter.dart` + +**Purpose:** The primary transport implementation for interacting with GenUI via **Streaming Text**. +**Used For:** Ideal for "Chat with LLM" scenarios where the model outputs a stream of text that may contain markdown, text, and JSON blocks mixed together. + +**Code Example:** + +``` +final transport = A2uiTransportAdapter( + onSend: (msg) => myLLMClient.sendMessage(msg), +); +// Feed raw text chunks (e.g. from a streaming API response) +llmStream.listen((chunk) => transport.addChunk(chunk)); +``` + +##### `A2uiTransportAdapter` + +- `void addChunk(String text)`: Feed text from LLM. +- `void addMessage(A2uiMessage message)`: Feed a raw A2UI message directly (e.g. from tool output). +- `Stream incomingText`: Stream of text content (markdown) with UI JSON blocks stripped out. +- `Stream incomingMessages`: Stream of parsed A2UI messages. +- `void dispose()`: Closes streams and cleans up resources. + +#### `lib/src/engine/surface_controller.dart` + +**Purpose:** The central engine for processing Structured A2UI Messages. +**Used For:** Use this directly when you have structured data instead of raw text. Common scenarios include: + +1. **Tool Use / Function Calling:** Your LLM returns parsed JSON arguments for a tool call. +2. **Non-LLM Backends:** Your server sends standard JSON payloads (like WebSockets). +3. **Static/Debug Content:** Rendering hardcoded component examples (e.g., `DebugCatalogView`). + +**Code Example:** + +``` +final controller = SurfaceController(catalogs: [myCatalog]); +// Feed a structured message object directly +controller.handleMessage( + UpdateComponents(surfaceId: 'main', components: [...]) +); +``` + +**Constructor Options used for Cleanup and Constraints:** + +- `cleanupStrategy`: Strategies for removing old surfaces (`ManualCleanupStrategy`, `KeepLastNCleanupStrategy`). +- `pendingUpdateTimeout`: Duration to wait for a `CreateSurface` message before discarding orphaned updates. + +##### `SurfaceController` + +- `Stream get onSubmit`: Stream of user interactions (form submissions). +- `Stream get surfaceUpdates`: Stream of events when surfaces change. +- `ValueListenable watchSurface(String surfaceId)`: Get the notifier for a surface's UI definition. +- `SurfaceContext contextFor(String surfaceId)`: Get a scoped context for a specific surface. +- `void dispose()`: Cleans up surface notifiers and streams. +- `void handleMessage(A2uiMessage message)`: Processes an incoming `A2uiMessage` (create, update, delete surface). +- `void handleUiEvent(UiEvent event)`: Handle a UI event from a surface. + +##### `SurfaceHost` (Interface) + +- **The Contract:** Defines how surface managers interact with the backend logic. +- **API:** +- `Stream get surfaceUpdates`: Stream of events when surfaces change. +- `SurfaceContext contextFor(String surfaceId)`: Get a scoped context for a specific surface. + +##### `SurfaceContext` (Interface) + +- **The Contract:** Defines how `Surface` interacts with the backend logic, scoped to a single surface. +- **API:** +- `String get surfaceId`: The ID of the surface. +- `ValueListenable get definition`: The reactive definition of the UI. +- `DataModel get dataModel`: The data model for this surface. +- `Iterable get catalogs`: The catalogs available to this context. +- `void handleUiEvent(UiEvent event)`: Handle a UI event from a surface. + +##### `SurfaceUpdate` (Sealed Class) + +- Subclasses: `SurfaceAdded`, `ComponentsUpdated`, `SurfaceRemoved`. + +#### `lib/src/widgets/genui_surface.dart` + +**Purpose:** The Flutter widget that renders a dynamic UI surface. +**Used For:** Place this widget in your app where you want the AI-generated UI to appear. +**Code Example:** + +``` +Surface( + surfaceContext: myHost.contextFor('main-surface'), +) +``` + +##### `Surface` (StatefulWidget) + +- Constructor: `Surface({required SurfaceContext context, WidgetBuilder? defaultBuilder})` +- The `defaultBuilder` renders a placeholder while the surface definition is empty or loading. + + +#### `lib/src/facade/conversation.dart` + +**Purpose:** High-level abstraction for managing a chat conversation with GenUI support. +**Used For:** Building a chat app where the view binds to a list of messages. +**Code Example:** + +``` +final conversation = Conversation( + controller: myController, + transport: myTransport, +); +``` + +**`Conversation`** + +- `ValueListenable get state`: The current state (surfaces, latest text, isWaiting). +- `Stream get events`: A stream of events. +- `Future sendRequest(ChatMessage message)`: Sends a message to the LLM. + +### Data Models & Protocol + +These classes define the data structures and protocol used by GenUI. + +#### `lib/src/model/data_model.dart` + +**Purpose:** The reactive data store for GenUI surfaces. +**Used For:** Managing state shared between components. + +##### `DataModel` + +- `void update(DataPath? path, Object? contents)`: Updates data. +- `ValueNotifier subscribe(DataPath path)`: Subscribe to changes. +- `ValueNotifier subscribeToValue(DataPath path)`: Subscribe to changes at a specific path only. +- `T? getValue(DataPath path)`: Retrieve a static value without subscribing. +- `void bindExternalState({required DataPath path, required ValueListenable source, bool twoWay})`: Bind an external `ValueNotifier` to the data model. +- `void dispose()`: Disposes resources. + +##### `DataPath` + +- Parses and represents paths like `/user/name` or relative paths. + +##### `DataContext` + +- A view of the `DataModel` scoped to a specific path (used by widgets). + +#### `lib/src/model/ui_models.dart` + +**Purpose:** Core models for UI definition and events. + +##### `UiDefinition` + +- Represents the state of a surface: `catalogId`, `components` map, `theme`. + +##### `UiEvent` & `UserActionEvent` + +- Represents events triggered by the user (e.g. button click). + +##### `Component` + +- Data class for a single widget instance (type, id, properties). + +#### `lib/src/model/a2ui_message.dart` + +**Purpose:** Defines the messages exchanged in the A2UI protocol. +**Used For:** Parsing server responses. + +##### `A2uiMessage` (Sealed Class) + +- Subclasses: `CreateSurface`, `UpdateComponents`, `UpdateDataModel`, `DeleteSurface`. +- `factory fromJson(JsonMap json)`: Parses any A2UI message. + +#### `lib/src/model/generation_events.dart` + +**Purpose:** Events related to the generation process (tokens, tools, text). +**Used For:** Monitoring the stream from the LLM. + +##### `GenerationEvent` (Sealed Class) + +- Subclasses: `TextEvent`, `A2uiMessageEvent`, `ToolStartEvent`, `ToolEndEvent`, `TokenUsageEvent`. + +#### `lib/src/model/a2ui_client_capabilities.dart` + +**Purpose:** Describes the client's supported catalogs. +**Used For:** Sending client capabilities to the server/LLM. + +##### `A2UiClientCapabilities` + +- Hold list of `supportedCatalogIds`. + +#### `lib/src/model/a2ui_schemas.dart` + +**Purpose:** Provides pre-defined JSON schemas for common data types and validation. +**Used For:** Defining `CatalogItem` schemas concisely. + +##### `A2uiSchemas` + +- Static methods like `stringReference()`, `numberReference()`, `action()`, `updateComponentsSchema()`, etc. + +#### `lib/src/model/chat_message.dart` + +**Purpose:** Re-exports `genai_primitives` for chat message models. +**Used For:** Formatting messages for the UI or LLM. + +##### `ChatMessageFactories` + +- Helpers like `userText` and `modelText`. + +#### `lib/src/model/parts.dart` & `parts/ui.dart` + +**Purpose:** Extensions to `ChatMessage` parts to support UI payloads. +**Used For:** Handling multimodal messages that include UI definitions. + +##### `UiPart` + +- Wraps a `UiDefinition` in a message part. + +##### `UiInteractionPart` +- Wraps a user interaction event in a message part. + +### Catalogs & Component Infrastructure + +These classes handle the definition and building of UI components. + +#### `lib/src/model/catalog.dart` + +**Purpose:** Represents a collection of `CatalogItem`s. +**Used For:** Grouping widgets to provide to the `SurfaceController`. + +##### `Catalog` + +- `Schema get definition`: Generates the full JSON schema for the catalog (for the LLM). +- `Widget buildWidget(...)`: Builds a widget from the catalog given context. +- `Catalog copyWith(List newItems)`: Returns a new catalog with items added/replaced. +- `Catalog copyWithout(Iterable itemNames)`: Returns a new catalog with items removed. + +#### `lib/src/model/catalog_item.dart` + +**Purpose:** Defines a single UI component type. +**Used For:** Creating custom components. +**Code Example:** + +``` +final myItem = CatalogItem( + name: 'MyWidget', + dataSchema: S.object(...), + widgetBuilder: (context) => MyWidget(...), +); +``` + +##### `CatalogItem` + +- Properties: `name`, `dataSchema`, `widgetBuilder`, `exampleData`. + +##### `CatalogItemContext` +- Context object passed to `widgetBuilder`, containing `data`, `dataContext`, `buildChild`, etc. + +#### `lib/src/catalog/basic_catalog.dart` + +**Purpose:** Defines the `BasicCatalogItems` class which provides the standard set of A2UI components. +**Used For:** Use `BasicCatalogItems.asCatalog()` to get a ready-to-use catalog for your `SurfaceController`. +**Code Example:** + +``` +final controller = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], +); +``` + +##### `BasicCatalogItems` + +- `static Catalog asCatalog()`: Creates a `Catalog` containing all basic items (Button, Text, Column, etc.) with the standard A2UI catalog ID. + +#### `lib/src/functions/functions.dart` + +**Purpose:** Registry of client-side functions available to the A2UI expression system. +**Used For:** Register custom functions that the AI can invoke or use in expressions. + +##### `FunctionRegistry` + +- `void register(String name, ClientFunction function)`: Add a custom function. +- `Object? invoke(String name, List args)`: Call a function. +- `void registerStandardFunctions()`: Registers the default set of functions (e.g. `required`, `regex`, `length`, etc.). + +### Utilities & Helpers + +#### `lib/src/transport/a2ui_parser_transformer.dart` + +**Purpose:** A stream transformer that parses raw text chunks into `GenerationEvent`s. +**Used For:** Piping an LLM text stream into the `SurfaceController`. + +##### `A2uiParserTransformer` + +- Transforms `Stream` \-\> `Stream`. Handles JSON block extraction and balancing. + +#### `lib/src/functions/expression_parser.dart` + +**Purpose:** Evaluates `${...}` expressions and logic in A2UI definitions. +**Used For:** Internal use for resolving data bindings and executing client-side logic/validation. + +##### `ExpressionParser` + +- `Object? parse(String input)`: Parses a string with potential expressions. +- `bool evaluateLogic(JsonMap expression)`: Evaluates a logic object (and/or/not). +- `Object? evaluateFunctionCall(JsonMap callDefinition)`: Evaluates a function call map. + +#### `lib/src/utils/json_block_parser.dart` + +**Purpose:** Robustly extracts JSON from potentially messy LLM output. +**Used For:** Parsing JSON blocks even if surrounded by markdown or incomplete. + +##### `JsonBlockParser` + +- `static Object? parseFirstJsonBlock(String text)` +- `static List parseJsonBlocks(String text)` +- `static String stripJsonBlock(String text)` + +#### `lib/src/widgets/widget_utilities.dart` + +**Purpose:** Helpers for data binding and widgets. + +##### `DataContextExtensions` + +- `subscribeToValue`: Helper to create a `ValueNotifier` from a data path or literal. + +##### `OptionalValueBuilder` +- Helper widget to build children only when a value is non-null. + +#### `lib/src/core/prompt_fragments.dart` + +**Purpose:** Contains static strings useful for prompting the LLM. +**Used For:** Injecting instructions into the system prompt. + +##### `PromptFragments` + +- `basicChat`: A standard prompt block instructing the LLM to use UI tools. + +#### `lib/src/model/basic_catalog_embed.dart` + +**Purpose:** embedded text resource. +**Used For:** Accessing the basic catalog rules as a string for prompts. + +#### `lib/src/primitives/logging.dart` + +**Purpose:** Internal logging. **Used For:** Access `genUiLogger`. + +#### `lib/src/primitives/cancellation.dart` + +**Purpose:** Simple cancellation token pattern. +**Used For:** Cancelling streaming operations. + +##### `CancellationSignal` + +- Methods: `cancel()`, `addListener()`. + +#### `lib/src/primitives/constants.dart` + +**Purpose:** Shared constants. +**Used For:** Accessing `basicCatalogId`. + +#### `lib/src/primitives/simple_items.dart` + +**Purpose:** Typedefs and simple utilities. +**Used For:** `JsonMap` typedef, `generateId()`. + +#### `lib/src/widgets/fallback_widget.dart` + +**Purpose:** Generic fallback widget for errors/loading. +**Used For:** Displaying errors within the GenUI area. + +##### `FallbackWidget` + +- Parameters: `error`, `isLoading`, `onRetry`. + +#### `lib/src/facade/widgets/chat_primitives.dart` + +**Purpose:** Basic widgets for displaying chat messages. +**Used For:** Quickly building a chat interface. + +##### `ChatMessageView` + +- Displays a simple user or model text message. + +##### `InternalMessageView` +- Displays system/debug messages. + +### Tooling & Integrations + +#### `lib/src/facade/direct_call_integration/model.dart` + +**Purpose:** Models for parsing tool calls when using "Direct Tool Call" LLM APIs (like OpenAI function calling). + +##### `ToolCall` + +- Represents a call to a tool with name and arguments. + +##### `ClientFunction` +- Represents the schema of a tool to be sent to the LLM. + +#### `lib/src/facade/direct_call_integration/utils.dart` + +**Purpose:** Utilities for integrating with LLM tool-calling APIs. + +##### `genUiTechPrompt` + +- Generates a system prompt explaining how to use the UI tools. + +##### `catalogToFunctionDeclaration` +- Converts a `Catalog` into a `ClientFunction` for the LLM. + +#### `lib/src/development_utilities/catalog_view.dart` + +**Purpose:** A widget for visualizing all items in a catalog using their example data. +**Used For:** Development and debugging of custom catalogs. + +##### `DebugCatalogView` + +- Renders a list of all components in the provided `Catalog` by rendering their `exampleData`. + +### Standard Catalog Items + +These are the standard widgets available in the `CoreCatalog`: + +* `audioPlayer` +* `button` +* `card` +* `checkBox` +* `choicePicker` +* `column` +* `dateTimeInput` +* `divider` +* `icon` +* `image` + +* `list` +* `modal` +* `row` +* `slider` +* `tabs` +* `text` +* `textField` +* `video` + +#### `lib/src/catalog/core_widgets/widget_helpers.dart` + +**Purpose:** Utilities for building standard widget structures like lists with templates. +**Used For:** Used internally by `Column`, `Row`, `List` to handle children building. + +##### `ComponentChildrenBuilder` + +- A widget that builds children from either an explicit list of IDs or a data-bound template. + +##### `buildWeightedChild` + +- Helper to wrap a child in `Flexible` if the component definition has a 'weight' property. + +## Testing & Validation Strategy + +The decoupled architecture requires a robust testing strategy that validates each layer independently and the system as a whole. This is split into **Unit Tests** for the Dart code and **LLM Evals** to verify that models can correctly "speak" the A2UI protocol. + +### 1. Dart Unit Tests + +We will maintain comprehensive unit test coverage for the framework components, adding new tests as we add new features and fix issues. PRs are required to add new tests and fix existing tests when code changes are made. + +### 2. LLM Evals & Validation + +Since the "backend" is now any LLM, we must ensure that models can reliably generate valid A2UI commands. We will use a dedicated **Evaluation Framework**, which will be detailed in a separate document. + +#### Evaluation Goal + +Measure the "A2UI Compliance Rate" of different models when given standard prompts, and track the results over time, ensuring that the framework produces valid A2UI commands at least 95% of the time on three foundation models (Gemini, ChatGPT, Claude). + +#### Evaluation Methodology +1. **Dataset:** A collection of `(Prompt, Expected UI Structure)` pairs. + * *Example:* "Create a login form" -> Expect `CreateSurface` with `TextField(username)`, `TextField(password)`, `Button(Login)`. +2. **Execution:** + * Send prompt + standard system instructions (from `PromptFragments`) using the `SurfaceController` logic. + * Capture the output stream. +3. **Validation Metrics:** + * **Syntax Validity:** Is the output valid JSON? + * **Protocol Compliance:** Does it adhere to the A2UI schema (correct message types, `version: "v0.9"`)? + * **Logic Correctness:** Does the generated UI match the intent? (e.g., does the login form actually have a password field?) + * **Round-Trip Validity:** Can the generated output be successfully parsed by `A2uiMessage.fromJson` without throwing? + +#### CI Integration +* Run a lightweight subset of evals (using a fast model) on PRs to catch regressions in the system prompts or schema definitions. +* Run a full evaluation suite nightly to track model performance over time. diff --git a/docs/REVIEWING_GUIDE.md b/docs/REVIEWING_GUIDE.md new file mode 100644 index 000000000..b86fbdbe2 --- /dev/null +++ b/docs/REVIEWING_GUIDE.md @@ -0,0 +1,421 @@ +# GenUI v0.9 Migration & BYO LLM Reviewing Guide + +This guide outlines the changes in the `packages/genui` package, focusing on the migration to A2UI Protocol v0.9 and the architectural shift to a "Bring Your Own LLM" model. + +## 1. High-Level Architecture Changes + +The core change is the decoupling of the AI client implementation from the GenUI framework. + +- **Old Mechanism**: `GenUiConversation` depended on `ContentGenerator` (or `AiClient`), which was responsible for parsing and transport. +- **New Mechanism**: `Conversation` depends on a `Transport` interface. + - **`Transport`**: A simple interface (`incomingText`, `incomingMessages`, `sendRequest`) that you can implement to bridge *any* LLM SDK to GenUI. + - **`A2uiTransportAdapter`**: A helper implementation provided for convenience. + +## 2. Key Themes + +1. **Protocol v0.9**: Strict adherence to the v0.9 A2UI specification. + - Messages are now strongly typed sealed classes (`CreateSurface`, `UpdateComponents`, etc.) in `a2ui_message.dart`. + - Validation is built-in. +2. **"Basic" Catalog**: The "Standard" catalog has been renamed to "Basic" catalog to better reflect its role as a minimal set of fundamental widgets. +3. **Renames**: Removal of the `GenUi` prefix from most classes to clean up the API (e.g., `GenUiConversation` -> `Conversation`, `GenUiSurface` -> `Surface`). +4. **Consolidation**: `MultipleChoice`, `CheckboxGroup`, and `RadioGroup` patterns are consolidated into `ChoicePicker`. +5. **Strict Class Modifiers**: To improve safety and signal intent, we now use specific modifiers: + * **`final`**: Applied to data classes (`SurfaceDefinition`, `Component`) and leaf nodes of event hierarchies to prevent extension. + * **`sealed`**: Applied to base classes of event hierarchies (`A2uiMessage`, `SurfaceUpdate`, `ConversationEvent`) to enable exhaustive pattern matching. + * **`interface`**: Applied to logic/facade classes (`SurfaceController`, `Conversation`, `DataModel`, `Catalog`) to allow mocking (`implements`) while discouraging inheritance (`extends`). + +## 3. Deletions (The "Clean Slate") + +Several packages and examples have been removed to support the "Bring Your Own LLM" model. The logic previously contained in these provider-specific packages should now be implemented by the user using the `Transport` interface, or via separate provider-specific adapter packages (future work). + +### Deleted Packages + +* **`packages/genui_google_generative_ai`**: Removed. +* **`packages/genui_firebase_ai`**: Removed. +* **`packages/genui_dartantic`**: Removed. + +### Deleted Examples + +* **`examples/simple_chat`**: Removed. + +## 4. Recommended Review Order + +To make sense of this large PR, I recommend reviewing files in this specific order: + +### Phase 1: The New Core Models (The "What") +Start here to understand the data structures driving the system. + +1. **`lib/src/model/a2ui_message.dart`** + * **What to look for**: Sealed class hierarchy (`A2uiMessage`, `CreateSurface`, etc.). Note the `v0.9` version check and json parsing logic. +2. **`lib/src/model/ui_models.dart`** + * **What to look for**: `SurfaceDefinition`, `Component` (renamed from `UiComponent`?), and `UiEvent`. These are now `final` classes. `SurfaceUpdate` is `sealed`. +3. **`lib/src/interfaces/transport.dart`** + * **What to look for**: The clean `Transport` interface definition. +4. **`lib/src/model/generation_events.dart`** (New) + * **What to look for**: The `GenerationEvent` sealed class hierarchy (`TextEvent`, `ToolStartEvent`, etc.) used by `Transport`. + +### Phase 2: The Facade (The "How User Uses It") +This shows how the changes affect the public API. + +5. **`lib/src/facade/conversation.dart`** + * **What to look for**: + * Replaces `GenUiConversation`. + * Constructor takes `SurfaceController` and `Transport`. + * Manages `ConversationState` (reactive state for the UI). + * Now an `interface class` to support mocking. +6. **`lib/src/facade/prompt_builder.dart`** (New) + * **What to look for**: Helper for constructing usage prompts (system instructions) separate from the conversation logic. +7. **`lib/src/model/basic_catalog_embed.dart`** (New) + * **What to look for**: Static container for basic catalog rules, used by `PromptBuilder` to inject catalog instructions without file I/O. + +### Phase 3: The Engine (The "Brain") +This is where the logic lives. + +6. **`lib/src/engine/surface_controller.dart`** + * **What to look for**: + * Replaces `GenUiController` / `A2uiMessageProcessor`. + * `handleMessage`: routing logic for A2UI messages. + * `_registry` and `_store` management. + * Now an `interface class`. +7. **`lib/src/engine/surface_registry.dart`** + * **What to look for**: How surfaces are tracked and looked up. +8. **`lib/src/engine/data_model_store.dart`** + * **What to look for**: Centralized management of data models for multiple surfaces. +9. **`lib/src/engine/cleanup_strategy.dart`** (New) + * **What to look for**: Policy for cleaning up old surfaces (e.g., `MaxSurfacesCleanupStrategy`). + +### Phase 4: Use & Rendering (The "Visuals") +9. **`lib/src/widgets/surface.dart`** + * **What to look for**: Replaces `GenUiSurface`. Renders the `SurfaceDefinition` from the registry. +10. **`lib/src/catalog/basic_catalog.dart`** + * **What to look for**: The rename from `core_catalog.dart`. + * **Note**: Check `lib/src/catalog/basic_catalog_widgets/choice_picker.dart` to see the new consolidated selection component. + * `BasicCatalogItems` is now an `abstract final class` (static namespace). + +### Phase 5: Transport Implementation (The "Plumbing") +11. **`lib/src/transport/a2ui_parser_transformer.dart`** + * **What to look for**: Logic for splitting stream chunks and parsing JSON objects (handling split JSONs). +12. **`lib/src/transport/a2ui_transport_adapter.dart`** + * **What to look for**: The "glue" class that makes it easy to use standard stream-based LLM SDKs. + +## 5. Renames Cheat Sheet + +| Old Name | New Name | Notes | +| :--- | :--- | :--- | +| `GenUiConversation` | `Conversation` | Public Facade | +| `GenUiController` | `SurfaceController` | Engine | +| `GenUiSurface` | `Surface` | Widget | +| `GenUiContext` | `SurfaceContext` | Interface | +| `UiDefinition` | `SurfaceDefinition` | Model | +| `StandardCatalog` | `BasicCatalog` | Catalog | +| `MultipleChoice` | `ChoicePicker` | Component | +| `ContentGenerator` | `Transport` | *Conceptually replaced* | +| `AiClient` | `Transport` | *Conceptually replaced* | + +## 6. Deleted/Obsolete Files (Internal to `genui`) +These files have been removed internal to `genui`. Verify that their functionality is truly covered by the new architecture. + +* `content_generator.dart` -> Replaced by `Transport`. +* `genui_surface.dart` -> Replaced by `surface.dart`. +* `ui_tools.dart` -> Function calling logic moved to `functions/`. + +## 7. File-by-File Changes (Detailed Review) + +### `lib/` (Root) + +* **`genui.dart`** (Modified): + * **Change**: Complete overhaul of exports. Removed `GenUiConversation`, `GenUiController` exports. Added `Conversation`, `SurfaceController`, `Transport`, `PromptBuilder`. + * **Context**: Public API surface has changed entirely to support the new architecture. `A2uiParserTransformer` and `A2uiTransportAdapter` are also exported here for convenience. +* **`parsing.dart`** (New): + * **Change**: Added new library for parsing utilities. Exports `JsonBlockParser`. + * **Context**: Focused on low-level parsing strategies. + +### `lib/src/catalog` (Renamed & Refactored) + +* **`basic_catalog.dart`** (Renamed from `core_catalog.dart`): + * **Change**: Renamed class `StandardCatalog` -> `BasicCatalog` and `StandardCatalogItems` -> `BasicCatalogItems`. + * **Context**: "Basic" better reflects the minimalist nature of this widget set. +* **`basic_catalog_widgets/choice_picker.dart`** (New): + * **Change**: Introduces `ChoicePicker` component. + * **Context**: Replaces and consolidates `multiple_choice.dart`, `check_box_group.dart`, etc. Unifies selection logic. +* **`basic_catalog_widgets`** (Moved): + * Moved from `core_widgets`. Most files are simple moves, but some (like `check_box.dart`) have been modified to map to the new `BasicCatalog` structure. + +### `lib/src/engine` (The New Core) + +* **`surface_controller.dart`** (New): + * **Change**: Implements the main A2UI message processing loop. Handles `CreateSurface`, `UpdateComponents`, `UpdateDataModel`, `DeleteSurface`. + * **Context**: Replaces `GenUiController` and `A2uiMessageProcessor`. +* **`surface_registry.dart`** (New): + * **Change**: Manages active surfaces (`SurfaceDefinition` storage). + * **Context**: Decoupled from the controller to allow easier lookup. +* **`data_model_store.dart`** (New): + * **Change**: Manages data models (variables) for surfaces. + * **Context**: Centralized state management for the "UpdateDataModel" messages. +* **`cleanup_strategy.dart`** (New): + * **Change**: Defines surface cleanup policies (e.g., how many surfaces to keep in memory). + +* **Deletions in `lib/src/core/`**: + * `a2ui_message_processor.dart`: Logic moved to `SurfaceController`. + * `genui_surface.dart`: Replaced by `lib/src/widgets/surface.dart`. + * `prompt_fragments.dart`: Moved/Refactored into `PromptBuilder`. + * `ui_tools.dart`: Function calling logic moved to `lib/src/functions/`. + * `widget_utilities.dart`: Moved to `lib/src/widgets/`. + +### `lib/src/facade` (Public API) + +* **`conversation.dart`** (New): + * **Change**: High-level orchestrator. Connects `Transport` to `SurfaceController`. + * **Context**: Replaces `GenUiConversation`. +* **`prompt_builder.dart`** (New): + * **Change**: Helper to build the system prompt with A2UI instructions. + * **Context**: Decouples prompt logic from the conversation class. + +* **Deletions**: + * `gen_ui_conversation.dart`: Replaced by `conversation.dart`. + * `direct_call_integration/`: **Entire directory deleted**. The direct call integration mechanism is being replaced by standard tool calling. + +### `lib/src/functions` (New) + +* **`functions.dart`**, **`expression_parser.dart`** (New): + * **Change**: Implements client-side expression evaluation and function definitions for A2UI client-side logic. + * **Context**: Replaces the logic previously in `ui_tools.dart`. + +### `lib/src/interfaces` (New Contracts) + +* **`transport.dart`** (New): + * **Change**: Defines the `Transport` interface (`incomingMessages`, `sendRequest`). + * **Context**: The core abstraction for "Bring Your Own LLM". +* **`surface_host.dart`**, **`surface_context.dart`** (New): + * **Change**: Interfaces for widgets to interact with the engine. + * **Context**: Decouples the widget tree from the controller implementation. + +### `lib/src/model` (Data Structures) + +* **`a2ui_message.dart`** (Modified): + * **Change**: Converted to `sealed class` hierarchy for v0.9 (strict types). + * **Details**: + * `SurfaceUpdate` -> Renamed to `UpdateComponents`. + * `DataModelUpdate` -> Renamed to `UpdateDataModel`. + * `BeginRendering` -> Renamed to `CreateSurface`. + * `SurfaceDeletion` -> Renamed to `DeleteSurface`. + * All messages now include `version: 'v0.9'`. + * **Context**: The "source of truth" for the protocol. +* **`ui_models.dart`** (Modified): + * **Change**: `Component` structure flattened in JSON. + * **Details**: The `component` property (which contained the properties map) is gone. Now, properties are top-level keys relative to the component object (minus `id` and `type`). + * **Validation**: Extensive schema validation added to `SurfaceDefinition`. + * **Event Bus**: added `SurfaceUpdate` sealed classes (`SurfaceAdded`, `ComponentsUpdated`, `SurfaceRemoved`) for internal event handling. +* **`generation_events.dart`** (New): + * **Change**: Defines the event hierarchy (`ThinkingEvent`, `ToolStartEvent`, `A2uiMessageEvent`) emitted by the `Transport`. +* **`basic_catalog_embed.dart`** (New): + * **Change**: Embeds the basic catalog rules as a const string. + * **Context**: Simplifies `PromptBuilder` usage. +* **`parts/ui.dart`** (Modified): + * **Change**: `UiPart` and `UiInteractionPart` no longer extend `Part`. + * **Details**: They are now wrapper classes (views) around `DataPart` that use specific MIME types (`application/vnd.genui.ui+json`). This aligns with the `genai_primitives` standard. +* **`data_model.dart`** (Modified): + * **Change**: Updates to observing data changes. +* **`parts/image.dart`** (Deleted): + * **Change**: Image part handling is likely covered by generic `DataPart` or moved. + +### `lib/src/transport` (Default Implementation) + +* **`a2ui_transport_adapter.dart`** (New): + * **Change**: Adapts a generic `Stream` (from any LLM) into a `Transport`. + * **Context**: The bridge for users migrating from the old `ContentGenerator`. +* **`a2ui_parser_transformer.dart`** (New): + * **Change**: Stream transformer that parses partial JSON chunks. + * **Context**: Critical for handling streaming LLM output robustly. + +* **Deletions**: + * `content_generator.dart`: Replaced by `Transport`. + +### `lib/src/widgets` (Rendering) + +* **`surface.dart`** (New): + * **Change**: The main widget that renders a UI surface. + * **Context**: Replaces `GenUiSurface`. +* **`fallback_widget.dart`** (New): + * **Change**: Displayed when a component type is unknown or fails to render. +* **`widget_utilities.dart`** (New): + * **Change**: Introduced `OptionalValueBuilder` and `DataContext` extension methods (`subscribeToString`, etc.) that handle type coercion resiliently. + * **Context**: Moved/Refactored from `src/core/`. + +### `packages/genui_a2ui` (Adapter Package) +This package has been updated to use the new `genui` core. + +* **`lib/src/a2ui_agent_connector.dart`** (Modified): + * **Change**: deeply updated to use `genui` v0.9 models (`A2uiMessage`, `ChatMessage` parts). + * **Context**: Connects to an existing A2UI Agent (using `a2a` client) and streams responses. +* **`lib/src/a2ui_content_generator.dart`** (Deleted): + * **Change**: Removed. + * **Context**: The concept of `ContentGenerator` is replaced by `Transport`, but `A2uiAgentConnector` serves as the primary entry point for this package now. +* **`lib/src/logging_utils.dart`** (New): + * **Change**: Added log sanitization helpers. + + +## New Layout + +``` + +├── docs +│   └── assets +├── examples +│   ├── catalog_gallery +│   │   ├── build +│   │   │   ├── native_assets +│   │   │   │   └── flutter-tester +│   │   │   ├── test_cache +│   │   │   │   └── build +│   │   │   └── unit_test_assets +│   │   │   ├── fonts +│   │   │   └── shaders +│   │   ├── integration_test +│   │   ├── lib +│   │   ├── samples +│   │   └── test +│   │   └── src +│   ├── simple_chat +│   │   ├── build +│   │   │   ├── native_assets +│   │   │   │   └── flutter-tester +│   │   │   ├── test_cache +│   │   │   │   └── build +│   │   │   └── unit_test_assets +│   │   │   ├── fonts +│   │   │   └── shaders +│   │   ├── integration_test +│   │   │   └── samples +│   │   ├── lib +│   │   │   └── api_key +│   │   └── test +│   ├── travel_app +│   │   ├── assets +│   │   │   ├── booking_service +│   │   │   ├── prompt_images +│   │   │   └── travel_images +│   │   ├── build +│   │   │   ├── native_assets +│   │   │   │   └── flutter-tester +│   │   │   ├── test_cache +│   │   │   │   └── build +│   │   │   └── unit_test_assets +│   │   │   ├── assets +│   │   │   │   ├── booking_service +│   │   │   │   └── travel_images +│   │   │   ├── fonts +│   │   │   ├── packages +│   │   │   │   ├── flutter_math_fork +│   │   │   │   │   └── lib +│   │   │   │   │   └── katex_fonts +│   │   │   │   │   └── fonts +│   │   │   │   └── gpt_markdown +│   │   │   │   └── lib +│   │   │   │   └── fonts +│   │   │   └── shaders +│   │   ├── integration_test +│   │   ├── lib +│   │   │   └── src +│   │   │   ├── ai_client +│   │   │   ├── catalog +│   │   │   ├── config +│   │   │   ├── tools +│   │   │   │   └── booking +│   │   │   └── widgets +│   │   └── test +│   │   ├── ai_client +│   │   ├── goldens +│   │   ├── tools +│   │   │   └── hotels +│   │   └── widgets +│   └── verdure +│   ├── client +│   │   ├── assets +│   │   │   ├── fonts +│   │   │   └── images +│   │   └── lib +│   │   ├── core +│   │   │   └── theme +│   │   └── features +│   │   ├── ai +│   │   ├── screens +│   │   ├── state +│   │   └── widgets +│   └── server +│   ├── a2ui_extension +│   │   └── src +│   │   └── a2ui_ext +│   └── verdure +│   └── images +├── packages +│   ├── genai_primitives +│   │   ├── example +│   │   ├── lib +│   │   │   └── src +│   │   │   └── parts +│   │   └── test +│   ├── genui +│   │   ├── build +│   │   │   ├── native_assets +│   │   │   │   └── flutter-tester +│   │   │   ├── test_cache +│   │   │   │   └── build +│   │   │   └── unit_test_assets +│   │   │   └── shaders +│   │   ├── example +│   │   ├── lib +│   │   │   ├── src +│   │   │   │   ├── catalog +│   │   │   │   │   └── basic_catalog_widgets +│   │   │   │   ├── development_utilities +│   │   │   │   ├── engine +│   │   │   │   ├── facade +│   │   │   │   │   └── widgets +│   │   │   │   ├── functions +│   │   │   │   ├── interfaces +│   │   │   │   ├── model +│   │   │   │   │   └── parts +│   │   │   │   ├── primitives +│   │   │   │   ├── transport +│   │   │   │   ├── utils +│   │   │   │   └── widgets +│   │   │   └── test +│   │   └── test +│   │   ├── catalog +│   │   │   └── core_widgets +│   │   ├── core +│   │   ├── development_utilities +│   │   ├── engine +│   │   ├── facade +│   │   ├── functions +│   │   ├── model +│   │   ├── transport +│   │   └── utils +│   ├── genui_a2ui +│   │   ├── build +│   │   │   ├── native_assets +│   │   │   │   └── flutter-tester +│   │   │   ├── test_cache +│   │   │   │   └── build +│   │   │   └── unit_test_assets +│   │   │   ├── fonts +│   │   │   └── shaders +│   │   ├── lib +│   │   │   └── src +│   │   │   └── a2a +│   │   │   ├── client +│   │   │   └── core +│   │   └── test +│   │   └── a2a +│   │   ├── client +│   │   └── core +│   └── json_schema_builder +│   ├── bin +│   ├── example +│   ├── lib +│   │   └── src +│   │   └── schema +│   └── test +└── specs +``` \ No newline at end of file diff --git a/docs/assets/architecture.mmd b/docs/assets/architecture.mmd new file mode 100644 index 000000000..adfef0226 --- /dev/null +++ b/docs/assets/architecture.mmd @@ -0,0 +1,83 @@ +graph TD + %% --- Theme & Style Definitions --- + classDef default font-family:'Open Sans', sans-serif, font-size:24px, color:#0a305f; + + %% Regional Containers (Semi-transparent) + classDef developer fill:#f1f2f666,stroke:#2f3542,stroke-width:2px,stroke-dasharray: 5 5,rx:10; + classDef genui fill:#747d8c11,stroke:#2f3542,stroke-width:2px,rx:15; + + %% Components (Increased rx/ry for more breathing room) + classDef component fill:#769CDF,color:#0a305f,rx:8,ry:8; + classDef userComponent fill:#bec6dc,color:#283141,rx:8,ry:8; + classDef facade fill:#A288A6,color:#000,rx:8,ry:8; + classDef layerBox fill:#ffffff33,stroke:#747d8c,stroke-width:1px,stroke-dasharray: 2 2; + + subgraph DevApp ["Developer's Application"] + direction LR + AppLogic("App Logic / State Management"):::userComponent + LLMClient("External LLM Client"):::userComponent + AppUI("App UI (Flutter)"):::userComponent + end + + subgraph GenUIPkg ["GenUI Package"] + direction TB + Conversation("Conversation (Facade)"):::facade + + subgraph Transport ["Transport Layer"] + direction LR + TransportInt("Transport Interface"):::component + Adapter("A2uiTransportAdapter"):::component + Transformer("A2uiParserTransformer"):::component + end + + subgraph UILayer ["UI Layer"] + direction LR + Surface("Surface Widget"):::component + SurfaceContext("SurfaceContext"):::component + end + + subgraph Engine ["Engine Layer"] + direction LR + Controller("SurfaceController"):::component + Registry("SurfaceRegistry"):::component + DataModel("DataModel"):::component + Catalog("Component Catalog"):::component + end + end + + %% --- Functional Wiring --- + + AppLogic == "Initializes" ==> Conversation + AppLogic == "Sends User Input" ==> Conversation + + Conversation == "Forwards Request" ==> TransportInt + TransportInt == "Invokes Callback" ==> LLMClient + LLMClient == "Streams Response" ==> Adapter + Adapter == "Pipes Text" ==> Transformer + Transformer == "Parsed Events" ==> Adapter + Adapter == "Incoming Messages" ==> TransportInt + TransportInt == "Forwards Messages" ==> Conversation + + %% Layer Anchoring + Adapter == "Parsed Events" ==> Surface + AppUI == "Contains" ==> Surface + + Surface == "User Interaction" ==> SurfaceContext + SurfaceContext == "Forwards Event" ==> Controller + + Controller == "Updates" ==> Registry + Registry == "Notifies" ==> Surface + Controller == "Updates Protocol" ==> DataModel + SurfaceContext == "Accesses" ==> DataModel + Surface == "Builds Widgets" ==> Catalog + + Controller == "Emits Client Event" ==> Conversation + Conversation == "Auto-Replies (Loop)" ==> TransportInt + + %% Apply Classes + class DevApp developer; + class GenUIPkg genui; + class Transport,Engine,UILayer layerBox; + + %% Global line styling (Slightly thicker for the larger font scale) + linkStyle default stroke:#57606f,stroke-width:2.5px,font-size:22px; \ No newline at end of file diff --git a/docs/assets/architecture.svg b/docs/assets/architecture.svg new file mode 100644 index 000000000..eac448897 --- /dev/null +++ b/docs/assets/architecture.svg @@ -0,0 +1 @@ +

GenUI Package

Developer's Application

Engine Layer

UI Layer

Transport Layer

Initializes

Sends User Input

Forwards Request

Invokes Callback

Streams Response

Pipes Text

Parsed Events

Incoming Messages

Forwards Messages

Parsed Events

Contains

User Interaction

Forwards Event

Updates

Notifies

Updates Protocol

Accesses

Builds Widgets

Emits Client Event

Auto-Replies (Loop)

App Logic / State Management

External LLM Client

App UI (Flutter)

Conversation (Facade)

Transport Interface

A2uiTransportAdapter

A2uiParserTransformer

Surface Widget

SurfaceContext

SurfaceController

SurfaceRegistry

DataModel

Component Catalog

\ No newline at end of file diff --git a/docs/assets/class-diagram.mmd b/docs/assets/class-diagram.mmd new file mode 100644 index 000000000..5f985cf48 --- /dev/null +++ b/docs/assets/class-diagram.mmd @@ -0,0 +1,109 @@ +%%{init: {'theme': 'base', 'themeVariables': { 'primaryBorderColor': '#808080', 'lineColor': '#808080', 'background': 'transparent', 'mainBkg': '#ffffffaa', 'clusterBkg': 'transparent'}}}%% +classDiagram + namespace Facade { + class Conversation { + +SurfaceController controller + +Transport transport + +ValueListenable~ConversationState~ state + +Stream~ConversationEvent~ events + +Future~void~ sendRequest(ChatMessage message) + +void dispose() + } + } + + namespace Interfaces { + class Transport { + <> + +Stream~A2uiMessage~ incomingMessages + +Stream~String~ incomingText + +Future~void~ sendRequest(ChatMessage message) + } + + class SurfaceHost { + <> + +Stream~SurfaceUpdate~ surfaceUpdates + +SurfaceContext contextFor(String surfaceId) + } + + class SurfaceContext { + <> + +String surfaceId + +ValueListenable~UiDefinition?~ definition + +DataModel dataModel + +Iterable~Catalog~ catalogs + +void handleUiEvent(UiEvent event) + } + + class A2uiMessageSink { + <> + +void handleMessage(A2uiMessage message) + } + } + + namespace TransportDefinitions { + class A2uiTransportAdapter { + +void addChunk(String text) + +void addMessage(A2uiMessage message) + +Stream~A2uiMessage~ incomingMessages + +Stream~String~ incomingText + +Future~void~ sendRequest(ChatMessage message) + +void dispose() + } + } + + namespace Engine { + class SurfaceController { + +void handleMessage(A2uiMessage message) + +Stream~SurfaceUpdate~ surfaceUpdates + +Stream~ChatMessage~ onSubmit + +Iterable~String~ activeSurfaceIds + +SurfaceContext contextFor(String surfaceId) + +ValueListenable~UiDefinition?~ watchSurface(String surfaceId) + +void dispose() + } + } + + namespace UI { + class Surface { + +SurfaceContext context + +WidgetBuilder? defaultBuilder + } + } + + namespace Model { + class Catalog { + +String? catalogId + +Iterable~CatalogItem~ items + +Schema definition + +Widget buildWidget(CatalogItemContext context) + +Catalog copyWith(List~CatalogItem~ newItems) + +Catalog copyWithout(Iterable~CatalogItem~ itemNames) + } + + class CatalogItem { + +String name + +Schema dataSchema + +CatalogWidgetBuilder widgetBuilder + +List~ExampleBuilderCallback~ exampleData + } + + class DataModel { + +void update(DataPath? path, Object? contents) + +ValueNotifier subscribe(DataPath path) + +ValueNotifier subscribeToValue(DataPath path) + +T? getValue(DataPath path) + +void bindExternalState(DataPath path, ValueListenable source) + +void dispose() + } + } + + SurfaceHost <|.. SurfaceController + A2uiMessageSink <|.. SurfaceController + Transport <|.. A2uiTransportAdapter + Surface --> SurfaceContext : uses + SurfaceHost --> SurfaceContext : creates + Catalog --> CatalogItem : contains + SurfaceContext --> DataModel : manages + Conversation --> SurfaceController : uses + Conversation --> Transport : uses + SurfaceController --> Catalog : uses \ No newline at end of file diff --git a/docs/assets/class-diagram.svg b/docs/assets/class-diagram.svg new file mode 100644 index 000000000..17d8e71c5 --- /dev/null +++ b/docs/assets/class-diagram.svg @@ -0,0 +1 @@ +

Facade

Interfaces

TransportDefinitions

Engine

UI

Model

uses

creates

contains

manages

uses

uses

uses

Conversation

+SurfaceController controller

+Transport transport

+ValueListenable<ConversationState> state

+Stream<ConversationEvent> events

+Future<void> sendRequest(ChatMessage message)

+void dispose()

«interface»

Transport

+Stream<A2uiMessage> incomingMessages

+Stream<String> incomingText

+Future<void> sendRequest(ChatMessage message)

«interface»

SurfaceHost

+Stream<SurfaceUpdate> surfaceUpdates

+SurfaceContext contextFor(String surfaceId)

«interface»

SurfaceContext

+String surfaceId

+ValueListenable<UiDefinition?> definition

+DataModel dataModel

+Iterable<Catalog> catalogs

+void handleUiEvent(UiEvent event)

«interface»

A2uiMessageSink

+void handleMessage(A2uiMessage message)

A2uiTransportAdapter

+Stream<A2uiMessage> incomingMessages

+Stream<String> incomingText

+void addChunk(String text)

+void addMessage(A2uiMessage message)

+Future<void> sendRequest(ChatMessage message)

+void dispose()

SurfaceController

+Stream<SurfaceUpdate> surfaceUpdates

+Stream<ChatMessage> onSubmit

+Iterable<String> activeSurfaceIds

+void handleMessage(A2uiMessage message)

+SurfaceContext contextFor(String surfaceId)

+ValueListenable<UiDefinition?> watchSurface(String surfaceId)

+void dispose()

Surface

+SurfaceContext context

+WidgetBuilder? defaultBuilder

Catalog

+String? catalogId

+Iterable<CatalogItem> items

+Schema definition

+Widget buildWidget(CatalogItemContext context)

+Catalog copyWith(List<CatalogItem> newItems)

+Catalog copyWithout(Iterable<CatalogItem> itemNames)

CatalogItem

+String name

+Schema dataSchema

+CatalogWidgetBuilder widgetBuilder

+List<ExampleBuilderCallback> exampleData

DataModel

+void update(DataPath? path, Object? contents)

+ValueNotifier subscribe(DataPath path)

+ValueNotifier subscribeToValue(DataPath path)

+T? getValue(DataPath path)

+void bindExternalState(DataPath path, ValueListenable source)

+void dispose()

\ No newline at end of file diff --git a/docs/assets/surface-lifecycle.mmd b/docs/assets/surface-lifecycle.mmd new file mode 100644 index 000000000..7fd98ef78 --- /dev/null +++ b/docs/assets/surface-lifecycle.mmd @@ -0,0 +1,30 @@ +%%{init: {'theme': 'base', 'themeVariables': { 'primaryBorderColor': '#808080', 'lineColor': '#808080', 'background': 'transparent', 'mainBkg': '#ffffffaa', 'clusterBkg': 'transparent'}}}%% +sequenceDiagram + participant LLM as External LLM + participant Transport as Transport + participant Controller as SurfaceController + participant UI as Surface + + Note over Controller: Strategy: KeepLastN(1) + + LLM->>Transport: "createSurface(id: 'A')" + Transport->>Controller: CreateSurface('A') + Controller->>UI: SurfaceAdded('A') + UI->>UI: Render Surface A + + LLM->>Transport: "updateComponents(id: 'A', ...)" + Transport->>Controller: UpdateComponents('A') + Controller->>UI: ComponentsUpdated('A') + UI->>UI: Update Surface A + + LLM->>Transport: "createSurface(id: 'B')" + Transport->>Controller: CreateSurface('B') + + rect rgb(255, 240, 240) + Note right of Controller: Policy Enforcement + Controller->>UI: SurfaceRemoved('A') + UI->>UI: Dispose Surface A + end + + Controller->>UI: SurfaceAdded('B') + UI->>UI: Render Surface B \ No newline at end of file diff --git a/docs/assets/surface-lifecycle.svg b/docs/assets/surface-lifecycle.svg new file mode 100644 index 000000000..8c14d3aaf --- /dev/null +++ b/docs/assets/surface-lifecycle.svg @@ -0,0 +1 @@ +SurfaceSurfaceControllerTransportExternal LLMSurfaceSurfaceControllerTransportExternal LLMStrategy: KeepLastN(1)Policy Enforcement"createSurface(id: 'A')"CreateSurface('A')SurfaceAdded('A')Render Surface A"updateComponents(id: 'A', ...)"UpdateComponents('A')ComponentsUpdated('A')Update Surface A"createSurface(id: 'B')"CreateSurface('B')SurfaceRemoved('A')Dispose Surface ASurfaceAdded('B')Render Surface B \ No newline at end of file diff --git a/docs/migration_0.7.0_to_0.8.0.md b/docs/migration_0.7.0_to_0.8.0.md new file mode 100644 index 000000000..a5bef2d36 --- /dev/null +++ b/docs/migration_0.7.0_to_0.8.0.md @@ -0,0 +1,223 @@ +# Migration Guide: GenUI v0.7.0 to v0.8.0 + +This release introduces significant changes to GenUI, primarily driven by the adoption of **A2UI v0.9**. This new protocol version represents a fundamental shift from a "Structured Output First" philosophy to a **"Prompt First"** approach, designed to be embedded directly in an LLM's system prompt. + +In addition to protocol changes, the `genui` package architecture has been decoupled to provide greater flexibility. The `ContentGenerator` abstraction has been removed in favor of a clean separation between the **Engine** (`SurfaceController`), the **Facade** (`Conversation`), and the **Transport** (your connection to an LLM or Agent). + +## Key Highlights + +- **A2UI v0.9 Adoption**: Complete protocol overhaul for better LLM performance and token efficiency. +- **Architecture Decoupling**: `ContentGenerator` is gone. You now have full control over how you connect to your AI provider. +- **Strict Validation**: The protocol now enforces strict validation, requiring specific system prompt instructions. +- **Simplified Schema**: Components now use a flat structure (`"component": "Text"`) instead of nested keys (`"Text": {...}`). + +--- + +## 1. Dependency Changes + +The provider-specific packages that previously implemented `ContentGenerator` (e.g., `genui_dartantic`, `genui_google_generative_ai`, `genui_firebase_ai`) have been **removed**. + +**Action**: +- Remove dependencies on `genui_dartantic`, `genui_google_generative_ai`, etc., and replace them with direct usage of the underlying SDKs (e.g. `dartantic_ai`, `firebase_ai`, `google_generative_ai`, etc.). +- If you were using `genui_a2ui`, it is still supported but has been updated to use the new `A2uiAgentConnector` pattern, and no longer has a `ContentGenerator`. + +## 2. Replacing `ContentGenerator` + +In v0.7.0, `ContentGenerator` handled everything: prompt construction, LLM calling, and parsing. In v0.8.0, this is split. + +### The New Pattern: AI Client + `A2uiTransportAdapter` + +You are now encouraged to implement a simple AI client that wraps your specific LLM SDK. `genui` provides `A2uiTransportAdapter` to bridge the gap between raw text streams coming from your LLM and the `SurfaceController`. + +#### Example: Migrating from `DartanticContentGenerator` + +**Old Way (v0.7.0):** +```dart +final generator = DartanticContentGenerator(apiKey: '...'); +final controller = SurfaceController(generator: generator); +``` + +**New Way (v0.8.0):** +1. **Define a simple client** (or use your SDK directly). +2. **Wire it up** using `SurfaceController` (the engine) and `A2uiTransportAdapter` (parsing logic). + +```dart +// 1. Initialize the Engine (holds the state of the UI) +final catalog = BasicCatalogItems.asCatalog(); +final surfaceController = SurfaceController(catalogs: [catalog]); + +// 2. Initialize the Adapter (handles A2UI parsing) +final adapter = A2uiTransportAdapter(); + +// 3. Connect them +adapter.messageStream.listen(surfaceController.handleMessage); + +// 4. (Optional) Use a Facade for easier state management +// You can use the 'Conversation' facade which wraps the controller and a transport. +// OR manage the loop yourself as shown below: + +// ... In your chat loop ... +await for (final chunk in myAiClient.sendStream(prompt)) { + // Feed text chunks into the adapter + adapter.addChunk(chunk); +} +``` + +This gives you complete control over the chat history, error handling, and retry logic, which was previously hidden inside `ContentGenerator`. + +## 3. Reviewing System Prompts (CRITICAL) + +A2UI v0.9 relies heavily on specific instructions being present in the system prompt. If you don't include the schema and the rules, the LLM will likely generate invalid v0.9 JSON. + +**Action**: Ensure your system prompt includes: +1. **The A2UI Schema**: Generated from your catalog. +2. **The Standard Rules**: `StandardCatalogEmbed.standardCatalogRules`. + +```dart +final String a2uiSchema = A2uiMessage.a2uiMessageSchema(catalog).toJson(indent: ' '); + +final systemInstruction = ''' +You are a helpful assistant. + + +$a2uiSchema + + +${StandardCatalogEmbed.standardCatalogRules} + +${PromptFragments.basicChat} +'''; +``` + +## 4. Protocol & Schema Changes + +If you are manually constructing generic UI JSON or have hardcoded implementation details, distinct breaking changes exist. + +### `beginRendering` is now `createSurface` +- **Old**: `{ "beginRendering": { "root": "root", "styles": ... } }` +- **New**: `{ "createSurface": { "surfaceId": "...", "catalogId": "...", "theme": ... } }` + - Requires `catalogId`. + - Use `theme` instead of `styles`. + +### Component Definitions +- **Old**: `{ "Text": { "text": "Hello" } }` (Key-based) +- **New**: `{ "component": "Text", "text": "Hello" }` (Flat discriminator) + +### Data Binding +- **Old**: `{ "binding": "path/to/var" }` or `{ "literalString": "foo" }` +- **New**: Just use an object with a `path` property `{ "path": "/path/to/var" }` for path resolution or standard JSON types for literals. + +### Property Renames + +| Component | Old Property | New Property | +| :--- | :--- | :--- | +| **Row / Column** | `distribution` | `justify` | +| **Row / Column** | `alignment` | `align` | +| **Modal** | `entryPointChild` | `trigger` | +| **Modal** | `contentChild` | `content` | +| **TextField** | `text` | `value` | +| **Many** | `usageHint` | `variant` | +| **Client Actions** | `userAction` | `action` | + +## 5. Renames & Refactoring + +To improve clarity and reduce verbosity, many classes have been renamed to remove the `GenUi` prefix or align with standard Flutter/Dart conventions. + +| Old Name | New Name | Notes | +| :--- | :--- | :--- | +| `GenUiConversation` | `Conversation` | Collection of `ChatMessage`s. | +| `GenUiController` | `SurfaceController` | The core engine. | +| `GenUiSurface` | `Surface` | The widget that renders UI. | +| `GenUiHost` | `SurfaceHost` | Interface for the host environment. | +| `GenUiContext` | `SurfaceContext` | Context passed to components. | +| `GenUiTransport` | `Transport` | Interface for AI communication. | +| `ChatMessageWidget` | `ChatMessageView` | Widget for displaying messages. | +| `InternalMessageWidget` | `InternalMessageView` | Widget for internal system messages. | +| `GenUiFallback` | `FallbackWidget` | Error/Loading fallback. | +| `GenUiFunctionDeclaration` | `ClientFunction` | Tool declaration. | +| `GenUiPromptFragments` | `PromptFragments` | | +| `configureGenUiLogging` | `configureLogging` | | + +## 6. Using `genai_primitives` + +`genui` now builds upon the `genai_primitives` package for its core data structures. This unifies message types across the ecosystem. + +- **ChatMessage & Parts**: `ChatMessage`, `TextPart`, `DataPart`, etc., are now directly exported from `genai_primitives`. +- **UI Parts as Extensions**: `UiPart` and `UiInteractionPart` are no longer direct subclasses of `Part`. Instead, they are helper views over `DataPart` with specific MIME types. + +**Old Way:** +```dart +if (part is UiPart) { + // ... +} +``` + +**New Way:** +```dart +if (part.isUiPart) { + final uiPart = part.asUiPart!; // Returns a helper view + // access uiPart.definition +} +``` + +## 7. Connecting to Remote Agents + +If you are using `genui_a2ui` (A2A/A2UI adapter) to connect to a remote A2A/A2UI agent: + +**Old Way:** +```dart +final connector = GenUiA2uiConnector(url: ...); +``` + +**New Way:** +Use `A2uiAgentConnector`. + +```dart +final connector = A2uiAgentConnector(url: Uri.parse('...')); + +// Sending a message +final responseText = await connector.connectAndSend( + ChatMessage.user('Hello'), + clientCapabilities: ..., +); + +// Listening to the stream +connector.stream.listen((A2uiMessage message) { + // Pass to your SurfaceController/Adapter +}); +``` + +## 8. Example: Simple Chat Integration + +See `examples/simple_chat/lib/chat_session.dart` for a complete reference implementation of the new pattern. This example uses `dartantic_ai` as the LLM provider. + +### Quick Snippet + +```dart +class ChatSession { + final SurfaceController surfaceController; + final A2uiTransportAdapter transportAdapter; + + ChatSession() + : surfaceController = SurfaceController(catalogs: [CoreCatalogItems.asCatalog()]), + transportAdapter = A2uiTransportAdapter() { + + // Connect Adapter -> Controller + transportAdapter.messageStream.listen(surfaceController.handleMessage); + + // Listen for User Interactions + surfaceController.onSubmit.listen((event) { + // Handle button clicks / form submits + sendMessage(event.toString()); + }); + } + + Future sendMessage(String text) async { + // 1. Send to LLM + // 2. Stream response into transportAdapter + await for (final chunk in llm.stream(text)) { + transportAdapter.addChunk(chunk); + } + } +} +``` diff --git a/examples/README.md b/examples/README.md index cfcb44c9c..c7d32e802 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,14 +7,12 @@ capabilities. | Example | Complexity | Backend | Description | |---------|------------|---------|-------------| -| [catalog_gallery][catalog_gallery] | Simple | None | Visual reference for core catalog widgets | -| [custom_backend][custom_backend] | Intermediate | JSON `assets` | Demonstrates custom backend integration | +| [catalog_gallery][catalog_gallery] | Simple | None | Visual reference for basic catalog widgets | | [simple_chat][simple_chat] | Intermediate | Firebase/Google AI | A simple, conversational chat application. | | [travel_app][travel_app] | Advanced | Firebase/Google AI | Full travel planning assistant with custom catalog | | [verdure][verdure] | Advanced | Python A2A Server | Full-stack landscape design agent | [catalog_gallery]: https://github.com/flutter/genui/tree/main/examples/catalog_gallery -[custom_backend]: https://github.com/flutter/genui/tree/main/examples/custom_backend [simple_chat]: https://github.com/flutter/genui/tree/main/examples/simple_chat [travel_app]: https://github.com/flutter/genui/tree/main/examples/travel_app [verdure]: https://github.com/flutter/genui/tree/main/examples/verdure @@ -28,8 +26,7 @@ its specific instructions. | Goal | Recommended Example | |------|---------------------| -| Understand core widgets | `catalog_gallery` | +| Understand basic widgets | `catalog_gallery` | | Basic use of the SDK | `simple_chat` | -| Build custom backend integration | `custom_backend` | | Build a production-like app with custom catalog | `travel_app` | | Implement client-server architecture with A2A | `verdure` | diff --git a/examples/catalog_gallery/README.md b/examples/catalog_gallery/README.md index 4f8beebb6..57276162c 100644 --- a/examples/catalog_gallery/README.md +++ b/examples/catalog_gallery/README.md @@ -1,9 +1,9 @@ # catalog_gallery -A developer tool for visualizing and testing the core widget catalog. Displays all available `CoreCatalogItems` widgets and allows interaction testing. +A developer tool for visualizing and testing the basic widget catalog. Displays all available `BasicCatalogItems` widgets and allows interaction testing. **Key Features:** -- Browse all core catalog widgets +- Browse all basic catalog widgets - Interactive widget testing with event logging - Sample file loading support diff --git a/examples/catalog_gallery/integration_test/app_test.dart b/examples/catalog_gallery/integration_test/app_test.dart new file mode 100644 index 000000000..831b27c87 --- /dev/null +++ b/examples/catalog_gallery/integration_test/app_test.dart @@ -0,0 +1,176 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:catalog_gallery/main.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:logging/logging.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Configure logging + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + const fs = LocalFileSystem(); + Directory? samplesDir; + + // Locate samples directory synchronously before tests run + final Directory current = fs.currentDirectory; + if (current.childDirectory('samples').existsSync()) { + samplesDir = current.childDirectory('samples'); + } else if (current.childDirectory('../samples').existsSync()) { + samplesDir = current.childDirectory('../samples'); + } else if (current.path.endsWith('/integration_test')) { + final Directory parent = current.parent; + if (parent.childDirectory('samples').existsSync()) { + samplesDir = parent.childDirectory('samples'); + } + } + + if (samplesDir == null || !samplesDir.existsSync()) { + testWidgets('Samples directory validation', (tester) async { + fail('Could not find samples directory. CWD: ${current.path}'); + }); + return; + } + + // Filter for .sample files + final List files = samplesDir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.sample')) + .toList(); + + files.sort((a, b) => a.path.compareTo(b.path)); + + testWidgets('catalog_gallery smoke test - verify initial state', ( + tester, + ) async { + await tester.pumpWidget(CatalogGalleryApp(samplesDir: samplesDir, fs: fs)); + await tester.pumpAndSettle(); + + expect(find.text('Catalog Gallery'), findsOneWidget); + expect(find.text('Samples'), findsOneWidget); + }); + + group('Sample Rendering Tests', () { + for (final file in files) { + final String fileName = fs.path.basename(file.path); + testWidgets('Render sample: $fileName', (WidgetTester tester) async { + genUiLogger.info('Starting test for $fileName'); + + // Start the app with specific samples directory + await tester.pumpWidget( + CatalogGalleryApp(key: UniqueKey(), samplesDir: samplesDir, fs: fs), + ); + await tester.pumpAndSettle(); + + // Switch to the Samples tab. + await tester.tap(find.text('Samples')); + await tester.pumpAndSettle(); + + // Find the sample in the list. + final String displayName = fs.path.basenameWithoutExtension(file.path); + + final Finder sampleItemFinder = find.widgetWithText( + ListTile, + displayName, + ); + + // Scroll to the item if needed. + // The samples list is the first ListView in the hierarchy. + await tester.scrollUntilVisible( + sampleItemFinder, + 500, + scrollable: find + .descendant( + of: find.byType(ListView).first, + matching: find.byType(Scrollable), + ) + .first, + ); + await tester.pumpAndSettle(); + + expect( + sampleItemFinder, + findsOneWidget, + reason: 'Sample $displayName should be visible in the list', + ); + + // Tap the sample + await tester.tap(sampleItemFinder); + await tester.pumpAndSettle(); + + // Verify content + final String content = file.readAsStringSync(); + final List expectedTexts = _extractExpectedText(content); + final List expectedIds = _extractComponentIds(content); + + // Verify text content + for (final text in expectedTexts) { + if (find.text(text).evaluate().isEmpty) { + // Optional warning logging + } + } + + final Set ignoredIds = _ignoredIds[fileName] ?? {}; + for (final id in expectedIds) { + if (ignoredIds.contains(id)) { + continue; + } + if (find + .byKey(ValueKey(id), skipOffstage: false) + .evaluate() + .isEmpty) { + fail('Expected component with ID "$id" to be in the widget tree.'); + } + } + }); + } + }); +} + +final Map> _ignoredIds = { + 'settingsPage.sample': { + 'deleteConfirmationContent', + 'confirmationText', + 'modalButtonsRow', + 'confirmDeletionButton', + 'confirmDeletionButtonText', + 'cancelDeletionButton', + 'cancelDeletionButtonText', + }, +}; + +List _extractExpectedText(String content) { + final List result = []; + // Basic regex to find "text": "value" + final exp = RegExp(r'"text":\s*"([^"]+)"'); + for (final Match m in exp.allMatches(content)) { + if (m.groupCount >= 1) { + result.add(m.group(1)!); + } + } + return result; +} + +List _extractComponentIds(String content) { + final List result = []; + // Basic regex to find "id": "value" + final exp = RegExp(r'"id":\s*"([^"]+)"'); + for (final Match m in exp.allMatches(content)) { + if (m.groupCount >= 1) { + result.add(m.group(1)!); + } + } + return result; +} diff --git a/examples/catalog_gallery/ios/Podfile b/examples/catalog_gallery/ios/Podfile new file mode 100644 index 000000000..620e46eba --- /dev/null +++ b/examples/catalog_gallery/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/examples/catalog_gallery/lib/main.dart b/examples/catalog_gallery/lib/main.dart index a3b301f8b..4af3158e8 100644 --- a/examples/catalog_gallery/lib/main.dart +++ b/examples/catalog_gallery/lib/main.dart @@ -41,63 +41,87 @@ class CatalogGalleryApp extends StatefulWidget { super.key, this.samplesDir, this.fs = const LocalFileSystem(), + this.splashFactory, }); + final InteractiveInkFeatureFactory? splashFactory; + @override State createState() => _CatalogGalleryAppState(); } class _CatalogGalleryAppState extends State { - final Catalog catalog = CoreCatalogItems.asCatalog(); + final Catalog catalog = BasicCatalogItems.asCatalog(); @override Widget build(BuildContext context) { final bool showSamples = widget.samplesDir != null && widget.samplesDir!.existsSync(); - final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue); return MaterialApp( - theme: ThemeData.light().copyWith(colorScheme: colorScheme), - darkTheme: ThemeData.dark().copyWith(colorScheme: colorScheme), - home: DefaultTabController( - length: showSamples ? 2 : 1, - child: Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text('Catalog Gallery'), - bottom: showSamples - ? const TabBar( - tabs: [ - Tab(text: 'Catalog'), - Tab(text: 'Samples'), - ], - ) - : null, - ), - body: TabBarView( - children: [ - DebugCatalogView( - catalog: catalog, - onSubmit: (message) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'User action: ' - '${jsonEncode(message.parts.last)}', - ), + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + splashFactory: widget.splashFactory, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.blue, + brightness: Brightness.dark, + ), + splashFactory: widget.splashFactory, + ), + home: Builder( + builder: (context) { + return DefaultTabController( + length: showSamples ? 2 : 1, + child: Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.secondary, + title: Text( + 'Catalog Gallery', + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + ), + ), + bottom: showSamples + ? TabBar( + labelColor: Theme.of(context).colorScheme.onSecondary, + unselectedLabelColor: Theme.of( + context, + ).colorScheme.onSecondary.withValues(alpha: 0.5), + tabs: const [ + Tab(text: 'Catalog'), + Tab(text: 'Samples'), + ], + ) + : null, + ), + body: TabBarView( + children: [ + DebugCatalogView( + catalog: catalog, + onSubmit: (message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'User action: ' + '${jsonEncode(message.parts.last)}', + ), + ), + ); + }, + ), + if (showSamples) + SamplesView( + samplesDir: widget.samplesDir!, + catalog: catalog, + fs: widget.fs, ), - ); - }, + ], ), - if (showSamples) - SamplesView( - samplesDir: widget.samplesDir!, - catalog: catalog, - fs: widget.fs, - ), - ], - ), - ), + ), + ); + }, ), ); } diff --git a/examples/catalog_gallery/lib/sample_parser.dart b/examples/catalog_gallery/lib/sample_parser.dart index 7f8a2b530..7f7860b9d 100644 --- a/examples/catalog_gallery/lib/sample_parser.dart +++ b/examples/catalog_gallery/lib/sample_parser.dart @@ -28,7 +28,12 @@ class SampleParser { static Sample parseString(String content) { final List lines = const LineSplitter().convert(content); - final int separatorIndex = lines.indexOf('---'); + var startLine = 0; + if (lines.isNotEmpty && lines.first.trim() == '---') { + startLine = 1; + } + + final int separatorIndex = lines.indexOf('---', startLine); if (separatorIndex == -1) { throw const FormatException( @@ -37,10 +42,13 @@ class SampleParser { ); } - final String yamlHeader = lines.sublist(0, separatorIndex).join('\n'); + final String yamlHeader = lines + .sublist(startLine, separatorIndex) + .join('\n'); final String jsonlBody = lines.sublist(separatorIndex + 1).join('\n'); - final header = loadYaml(yamlHeader) as YamlMap; + final dynamic yamlNode = loadYaml(yamlHeader); + final Map header = (yamlNode is Map) ? yamlNode : {}; final String name = header['name'] as String? ?? 'Untitled Sample'; final String description = header['description'] as String? ?? ''; diff --git a/examples/catalog_gallery/lib/samples_view.dart b/examples/catalog_gallery/lib/samples_view.dart index 446c0e71a..314018c28 100644 --- a/examples/catalog_gallery/lib/samples_view.dart +++ b/examples/catalog_gallery/lib/samples_view.dart @@ -31,16 +31,16 @@ class _SamplesViewState extends State { List _sampleFiles = []; File? _selectedFile; Sample? _selectedSample; - late A2uiMessageProcessor _a2uiMessageProcessor; + late SurfaceController _genUiController; final List _surfaceIds = []; int _currentSurfaceIndex = 0; - StreamSubscription? _surfaceSubscription; + StreamSubscription? _surfaceSubscription; StreamSubscription? _messageSubscription; @override void initState() { super.initState(); - _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [widget.catalog]); + _genUiController = SurfaceController(catalogs: [widget.catalog]); _loadSamples(); _setupSurfaceListener(); } @@ -49,14 +49,12 @@ class _SamplesViewState extends State { void dispose() { _surfaceSubscription?.cancel(); _messageSubscription?.cancel(); - _a2uiMessageProcessor.dispose(); + _genUiController.dispose(); super.dispose(); } void _setupSurfaceListener() { - _surfaceSubscription = _a2uiMessageProcessor.surfaceUpdates.listen(( - update, - ) { + _surfaceSubscription = _genUiController.surfaceUpdates.listen((update) { if (update is SurfaceAdded) { if (!_surfaceIds.contains(update.surfaceId)) { setState(() { @@ -109,13 +107,14 @@ class _SamplesViewState extends State { _surfaceIds.clear(); _currentSurfaceIndex = 0; }); - // Re-create A2uiMessageProcessor to ensure a clean state for the new + // Re-create SurfaceController to ensure a clean state for the new // sample. - _a2uiMessageProcessor.dispose(); - _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [widget.catalog]); + _genUiController.dispose(); + _genUiController = SurfaceController(catalogs: [widget.catalog]); _setupSurfaceListener(); try { + genUiLogger.info('Displaying sample in ${file.basename}'); final Sample sample = await SampleParser.parseFile(file); setState(() { _selectedFile = file; @@ -123,21 +122,23 @@ class _SamplesViewState extends State { }); _messageSubscription = sample.messages.listen( - _a2uiMessageProcessor.handleMessage, + _genUiController.handleMessage, onError: (Object e) { - debugPrint('Error processing message: $e'); + genUiLogger.severe('Error processing message: $e'); if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error processing sample: $e')), ); }, ); - } catch (e) { - debugPrint('Error parsing sample: $e'); + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Error parsing sample in file ${file.path}: $exception\n$stackTrace', + ); if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error parsing sample: $e'))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error parsing sample: $exception')), + ); } } @@ -227,10 +228,15 @@ class _SamplesViewState extends State { Expanded( child: _surfaceIds.isEmpty ? const Center(child: Text('No surfaces')) - : GenUiSurface( - key: ValueKey(_surfaceIds[_currentSurfaceIndex]), - host: _a2uiMessageProcessor, - surfaceId: _surfaceIds[_currentSurfaceIndex], + : SingleChildScrollView( + child: Surface( + key: ValueKey( + _surfaceIds[_currentSurfaceIndex], + ), + surfaceContext: _genUiController.contextFor( + _surfaceIds[_currentSurfaceIndex], + ), + ), ), ), ], diff --git a/examples/catalog_gallery/linux/flutter/generated_plugin_registrant.cc b/examples/catalog_gallery/linux/flutter/generated_plugin_registrant.cc index e71a16d23..f6f23bfe9 100644 --- a/examples/catalog_gallery/linux/flutter/generated_plugin_registrant.cc +++ b/examples/catalog_gallery/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/examples/catalog_gallery/linux/flutter/generated_plugins.cmake b/examples/catalog_gallery/linux/flutter/generated_plugins.cmake index 2e1de87a7..f16b4c342 100644 --- a/examples/catalog_gallery/linux/flutter/generated_plugins.cmake +++ b/examples/catalog_gallery/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/catalog_gallery/macos/Flutter/Flutter-Debug.xcconfig b/examples/catalog_gallery/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b60..4b81f9b2d 100644 --- a/examples/catalog_gallery/macos/Flutter/Flutter-Debug.xcconfig +++ b/examples/catalog_gallery/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/catalog_gallery/macos/Flutter/Flutter-Release.xcconfig b/examples/catalog_gallery/macos/Flutter/Flutter-Release.xcconfig index c2efd0b60..5caa9d157 100644 --- a/examples/catalog_gallery/macos/Flutter/Flutter-Release.xcconfig +++ b/examples/catalog_gallery/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817a5..8236f5728 100644 --- a/examples/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/examples/catalog_gallery/macos/Podfile b/examples/catalog_gallery/macos/Podfile new file mode 100644 index 000000000..ff5ddb3b8 --- /dev/null +++ b/examples/catalog_gallery/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/examples/catalog_gallery/macos/Podfile.lock b/examples/catalog_gallery/macos/Podfile.lock new file mode 100644 index 000000000..ed05ac66c --- /dev/null +++ b/examples/catalog_gallery/macos/Podfile.lock @@ -0,0 +1,22 @@ +PODS: + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/examples/catalog_gallery/macos/Runner.xcodeproj/project.pbxproj b/examples/catalog_gallery/macos/Runner.xcodeproj/project.pbxproj index 1ef7b2696..e2479c28d 100644 --- a/examples/catalog_gallery/macos/Runner.xcodeproj/project.pbxproj +++ b/examples/catalog_gallery/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 72F592F52600B12767C2AD18 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1AA9715FD7CFD0A5228E5FE7 /* Pods_Runner.framework */; }; + 824EB1A60EABBB4DB02D887D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C18ED5F736CFD340C51CCC3D /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1AA9715FD7CFD0A5228E5FE7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* catalog_gallery.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "catalog_gallery.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* catalog_gallery.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = catalog_gallery.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 43F87F912A2812D64BCF8FC2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 492CF68E801DA70A60E84918 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5366D251A5FFFE7FA93C7B19 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 6EE4E885464A0837282A7EAD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C18ED5F736CFD340C51CCC3D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E774C36E5DD72A65BDBA90DB /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + F0CC54568539BA9B1D75CA8F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 824EB1A60EABBB4DB02D887D /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 72F592F52600B12767C2AD18 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,6 +137,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + A946C2B3F771898574BF1559 /* Pods */, ); sourceTree = ""; }; @@ -172,9 +185,25 @@ path = Runner; sourceTree = ""; }; + A946C2B3F771898574BF1559 /* Pods */ = { + isa = PBXGroup; + children = ( + 492CF68E801DA70A60E84918 /* Pods-Runner.debug.xcconfig */, + 6EE4E885464A0837282A7EAD /* Pods-Runner.release.xcconfig */, + 43F87F912A2812D64BCF8FC2 /* Pods-Runner.profile.xcconfig */, + F0CC54568539BA9B1D75CA8F /* Pods-RunnerTests.debug.xcconfig */, + 5366D251A5FFFE7FA93C7B19 /* Pods-RunnerTests.release.xcconfig */, + E774C36E5DD72A65BDBA90DB /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 1AA9715FD7CFD0A5228E5FE7 /* Pods_Runner.framework */, + C18ED5F736CFD340C51CCC3D /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 37073AD80C0F092DE9D54BCF /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + B4F1C3F289D3895F7C4CE3E0 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + 0B7297E2264320A7DD76C053 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -291,6 +323,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0B7297E2264320A7DD76C053 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -329,6 +378,50 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 37073AD80C0F092DE9D54BCF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B4F1C3F289D3895F7C4CE3E0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -380,6 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F0CC54568539BA9B1D75CA8F /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -394,6 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 5366D251A5FFFE7FA93C7B19 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -408,6 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = E774C36E5DD72A65BDBA90DB /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/examples/catalog_gallery/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/catalog_gallery/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16e..21a3cc14c 100644 --- a/examples/catalog_gallery/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/examples/catalog_gallery/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/examples/catalog_gallery/pubspec.yaml b/examples/catalog_gallery/pubspec.yaml index 24452c634..6a991cae9 100644 --- a/examples/catalog_gallery/pubspec.yaml +++ b/examples/catalog_gallery/pubspec.yaml @@ -24,6 +24,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + logging: ^1.3.0 flutter: uses-material-design: true diff --git a/examples/catalog_gallery/samples/animalKingdomExplorer.sample b/examples/catalog_gallery/samples/animalKingdomExplorer.sample new file mode 100644 index 000000000..a916b46fd --- /dev/null +++ b/examples/catalog_gallery/samples/animalKingdomExplorer.sample @@ -0,0 +1,42 @@ +--- +description: A simple, explicit UI to display a hierarchy of animals. +name: animalKingdomExplorer +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a simplified UI explorer for the Animal Kingdom. + + The UI must have a main 'Text' (variant 'h1') with the text "Simple Animal Explorer". + + Below the text heading, create a 'Tabs' component with exactly three tabs: "Mammals", "Birds", and "Reptiles". + + Each tab's content should be a 'Column'. The first item in each column must be a 'TextField' with the label "Search...". Below the search field, display the hierarchy for that tab using nested 'Card' components. + + The exact hierarchy to create is as follows: + + **1. "Mammals" Tab:** + - A 'Card' for the Class "Mammalia". + - Inside the "Mammalia" card, create two 'Card's for the following Orders: + - A 'Card' for the Order "Carnivora". Inside this, create 'Card's for these three species: "Lion", "Tiger", "Wolf". + - A 'Card' for the Order "Artiodactyla". Inside this, create 'Card's for these two species: "Giraffe", "Hippopotamus". + + **2. "Birds" Tab:** + - A 'Card' for the Class "Aves". + - Inside the "Aves" card, create three 'Card's for the following Orders: + - A 'Card' for the Order "Accipitriformes". Inside this, create a 'Card' for the species: "Bald Eagle". + - A 'Card' for the Order "Struthioniformes". Inside this, create a 'Card' for the species: "Ostrich". + - A 'Card' for the Order "Sphenisciformes". Inside this, create a 'Card' for the species: "Penguin". + + **3. "Reptiles" Tab:** + - A 'Card' for the Class "Reptilia". + - Inside the "Reptilia" card, create two 'Card's for the following Orders: + - A 'Card' for the Order "Crocodilia". Inside this, create a 'Card' for the species: "Nile Crocodile". + - A 'Card' for the Order "Squamata". Inside this, create 'Card's for these two species: "Komodo Dragon", "Ball Python". + + Each species card must contain a 'Row' with an 'Image' and a 'Text' component for the species name. Do not add any other components. + + Each Class and Order card must contain a 'Column' with a 'Text' component with the name, and then the children cards below. + + IMPORTANT: Do not skip any of the classes, orders, or species above. Include every item that is mentioned. + +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"List","children":["mainHeading","mainTabs"]},{"id":"mainHeading","component":"Text","text":"Simple Animal Explorer","variant":"h1"},{"id":"mainTabs","component":"Tabs","tabs":[{"title":"Mammals","child":"mammalsTabContent"},{"title":"Birds","child":"birdsTabContent"},{"title":"Reptiles","child":"reptilesTabContent"}]},{"id":"mammalsTabContent","component":"Column","children":["mammalsSearchField","mammaliaCard"]},{"id":"mammalsSearchField","component":"TextField","label":"Search...","value":""},{"id":"mammaliaCard","component":"Card","child":"mammaliaColumn"},{"id":"mammaliaColumn","component":"Column","children":["mammaliaText","carnivoraCard","artiodactylaCard"]},{"id":"mammaliaText","component":"Text","text":"Class: Mammalia"},{"id":"carnivoraCard","component":"Card","child":"carnivoraColumn"},{"id":"carnivoraColumn","component":"Column","children":["carnivoraText","lionCard","tigerCard","wolfCard"]},{"id":"carnivoraText","component":"Text","text":"Order: Carnivora"},{"id":"lionCard","component":"Card","child":"lionRow"},{"id":"lionRow","component":"Row","children":["lionImage","lionText"]},{"id":"lionImage","component":"Image","url":"https://example.com/lion.jpg"},{"id":"lionText","component":"Text","text":"Lion"},{"id":"tigerCard","component":"Card","child":"tigerRow"},{"id":"tigerRow","component":"Row","children":["tigerImage","tigerText"]},{"id":"tigerImage","component":"Image","url":"https://example.com/tiger.jpg"},{"id":"tigerText","component":"Text","text":"Tiger"},{"id":"wolfCard","component":"Card","child":"wolfRow"},{"id":"wolfRow","component":"Row","children":["wolfImage","wolfText"]},{"id":"wolfImage","component":"Image","url":"https://example.com/wolf.jpg"},{"id":"wolfText","component":"Text","text":"Wolf"},{"id":"artiodactylaCard","component":"Card","child":"artiodactylaColumn"},{"id":"artiodactylaColumn","component":"Column","children":["artiodactylaText","giraffeCard","hippopotamusCard"]},{"id":"artiodactylaText","component":"Text","text":"Order: Artiodactyla"},{"id":"giraffeCard","component":"Card","child":"giraffeRow"},{"id":"giraffeRow","component":"Row","children":["giraffeImage","giraffeText"]},{"id":"giraffeImage","component":"Image","url":"https://example.com/giraffe.jpg"},{"id":"giraffeText","component":"Text","text":"Giraffe"},{"id":"hippopotamusCard","component":"Card","child":"hippopotamusRow"},{"id":"hippopotamusRow","component":"Row","children":["hippopotamusImage","hippopotamusText"]},{"id":"hippopotamusImage","component":"Image","url":"https://example.com/hippopotamus.jpg"},{"id":"hippopotamusText","component":"Text","text":"Hippopotamus"},{"id":"birdsTabContent","component":"Column","children":["birdsSearchField","avesCard"]},{"id":"birdsSearchField","component":"TextField","label":"Search...","value":""},{"id":"avesCard","component":"Card","child":"avesColumn"},{"id":"avesColumn","component":"Column","children":["avesText","accipitriformesCard","struthioniformesCard","sphenisciformesCard"]},{"id":"avesText","component":"Text","text":"Class: Aves"},{"id":"accipitriformesCard","component":"Card","child":"accipitriformesColumn"},{"id":"accipitriformesColumn","component":"Column","children":["accipitriformesText","baldEagleCard"]},{"id":"accipitriformesText","component":"Text","text":"Order: Accipitriformes"},{"id":"baldEagleCard","component":"Card","child":"baldEagleRow"},{"id":"baldEagleRow","component":"Row","children":["baldEagleImage","baldEagleText"]},{"id":"baldEagleImage","component":"Image","url":"https://example.com/baldeagle.jpg"},{"id":"baldEagleText","component":"Text","text":"Bald Eagle"},{"id":"struthioniformesCard","component":"Card","child":"struthioniformesColumn"},{"id":"struthioniformesColumn","component":"Column","children":["struthioniformesText","ostrichCard"]},{"id":"struthioniformesText","component":"Text","text":"Order: Struthioniformes"},{"id":"ostrichCard","component":"Card","child":"ostrichRow"},{"id":"ostrichRow","component":"Row","children":["ostrichImage","ostrichText"]},{"id":"ostrichImage","component":"Image","url":"https://example.com/ostrich.jpg"},{"id":"ostrichText","component":"Text","text":"Ostrich"},{"id":"sphenisciformesCard","component":"Card","child":"sphenisciformesColumn"},{"id":"sphenisciformesColumn","component":"Column","children":["sphenisciformesText","penguinCard"]},{"id":"sphenisciformesText","component":"Text","text":"Order: Sphenisciformes"},{"id":"penguinCard","component":"Card","child":"penguinRow"},{"id":"penguinRow","component":"Row","children":["penguinImage","penguinText"]},{"id":"penguinImage","component":"Image","url":"https://example.com/penguin.jpg"},{"id":"penguinText","component":"Text","text":"Penguin"},{"id":"reptilesTabContent","component":"Column","children":["reptilesSearchField","reptiliaCard"]},{"id":"reptilesSearchField","component":"TextField","label":"Search...","value":""},{"id":"reptiliaCard","component":"Card","child":"reptiliaColumn"},{"id":"reptiliaColumn","component":"Column","children":["reptiliaText","crocodiliaCard","squamataCard"]},{"id":"reptiliaText","component":"Text","text":"Class: Reptilia"},{"id":"crocodiliaCard","component":"Card","child":"crocodiliaColumn"},{"id":"crocodiliaColumn","component":"Column","children":["crocodiliaText","nileCrocodileCard"]},{"id":"crocodiliaText","component":"Text","text":"Order: Crocodilia"},{"id":"nileCrocodileCard","component":"Card","child":"nileCrocodileRow"},{"id":"nileCrocodileRow","component":"Row","children":["nileCrocodileImage","nileCrocodileText"]},{"id":"nileCrocodileImage","component":"Image","url":"https://example.com/nilecrocodile.jpg"},{"id":"nileCrocodileText","component":"Text","text":"Nile Crocodile"},{"id":"squamataCard","component":"Card","child":"squamataColumn"},{"id":"squamataColumn","component":"Column","children":["squamataText","komodoDragonCard","ballPythonCard"]},{"id":"squamataText","component":"Text","text":"Order: Squamata"},{"id":"komodoDragonCard","component":"Card","child":"komodoDragonRow"},{"id":"komodoDragonRow","component":"Row","children":["komodoDragonImage","komodoDragonText"]},{"id":"komodoDragonImage","component":"Image","url":"https://example.com/komododragon.jpg"},{"id":"komodoDragonText","component":"Text","text":"Komodo Dragon"},{"id":"ballPythonCard","component":"Card","child":"ballPythonRow"},{"id":"ballPythonRow","component":"Row","children":["ballPythonImage","ballPythonText"]},{"id":"ballPythonImage","component":"Image","url":"https://example.com/ballpython.jpg"},{"id":"ballPythonText","component":"Text","text":"Ball Python"}]}} diff --git a/examples/catalog_gallery/samples/calendarEventCreator.sample b/examples/catalog_gallery/samples/calendarEventCreator.sample new file mode 100644 index 000000000..3df5df374 --- /dev/null +++ b/examples/catalog_gallery/samples/calendarEventCreator.sample @@ -0,0 +1,8 @@ +--- +description: A form to create a new calendar event. +name: calendarEventCreator +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a calendar event creation form. It should have a 'Text' (variant 'h1') "New Event". Include a 'TextField' for the "Event Title". Use a 'Row' for two 'DateTimeInput's for "Start Time" and "End Time" (initialize both with a literal empty string value: '' (do not bind to a data path)). Add a 'CheckBox' labeled "All-day event". Finally, a 'Row' with two 'Button's: "Save" and "Cancel". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["newEventTitle","eventTitleField","timeInputsRow","allDayCheckbox","actionButtonsRow"]},{"id":"newEventTitle","component":"Text","text":"New Event","variant":"h1"},{"id":"eventTitleField","component":"TextField","label":"Event Title","value":""},{"id":"timeInputsRow","component":"Row","children":["startTimeInput","endTimeInput"],"justify":"spaceBetween"},{"id":"startTimeInput","component":"DateTimeInput","value":"","enableDate":true,"enableTime":true,"label":"Start Time"},{"id":"endTimeInput","component":"DateTimeInput","value":"","enableDate":true,"enableTime":true,"label":"End Time"},{"id":"allDayCheckbox","component":"CheckBox","label":"All-day event","value":false},{"id":"actionButtonsRow","component":"Row","children":["saveButton","cancelButton"],"justify":"end"},{"id":"saveButton","component":"Button","child":"saveButtonText","action":{"event":{"name":"saveEvent"}},"variant":"primary"},{"id":"saveButtonText","component":"Text","text":"Save"},{"id":"cancelButton","component":"Button","child":"cancelButtonText","action":{"event":{"name":"cancelEvent"}}},{"id":"cancelButtonText","component":"Text","text":"Cancel"}]}} diff --git a/examples/catalog_gallery/samples/chatRoom.sample b/examples/catalog_gallery/samples/chatRoom.sample new file mode 100644 index 000000000..ae3d18f4f --- /dev/null +++ b/examples/catalog_gallery/samples/chatRoom.sample @@ -0,0 +1,8 @@ +--- +description: A chat application interface. +name: chatRoom +prompt: | + Create a chat room interface. It should have a 'Column' for the message history. Inside, include several 'Card's representing messages, each with a 'Text' for the sender and a 'Text' for the message body. Specifically include these messages: "Alice: Hi there!", "Bob: Hello!". At the bottom, a 'Row' with a 'TextField' (label "Type a message...") and a 'Button' labeled "Send". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["messageHistoryColumn","messageInputRow"]},{"id":"messageHistoryColumn","component":"Column","children":["messageCardAlice","messageCardBob"]},{"id":"messageCardAlice","component":"Card","child":"messageContentAlice"},{"id":"messageContentAlice","component":"Column","children":["senderAlice","bodyAlice"]},{"id":"senderAlice","component":"Text","text":"Alice:"},{"id":"bodyAlice","component":"Text","text":"Hi there!"},{"id":"messageCardBob","component":"Card","child":"messageContentBob"},{"id":"messageContentBob","component":"Column","children":["senderBob","bodyBob"]},{"id":"senderBob","component":"Text","text":"Bob:"},{"id":"bodyBob","component":"Text","text":"Hello!"},{"id":"messageInputRow","component":"Row","children":["messageTextField","sendButton"]},{"id":"messageTextField","component":"TextField","label":"Type a message...","value":""},{"id":"sendButton","component":"Button","child":"sendButtonText","action":{"event":{"name":"sendMessage"}}},{"id":"sendButtonText","component":"Text","text":"Send"}]}} diff --git a/examples/catalog_gallery/samples/checkoutPage.sample b/examples/catalog_gallery/samples/checkoutPage.sample new file mode 100644 index 000000000..7ebe9278e --- /dev/null +++ b/examples/catalog_gallery/samples/checkoutPage.sample @@ -0,0 +1,9 @@ +--- +description: A simplified e-commerce checkout page. +name: checkoutPage +prompt: | + Create a simplified e-commerce checkout page. It should have a 'Text' (variant 'h1') "Checkout". A 'Column' for shipping info with 'TextField's for "Name", "Address", "City", "Zip Code". A 'Column' for payment info with 'TextField's for "Card Number", "Expiry Date", "CVV". Finally, a 'Text' "Total: $99.99" and a 'Button' "Place Order". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["checkoutTitle","shippingInfoColumn","paymentInfoColumn","totalText","placeOrderButton"]},{"id":"checkoutTitle","component":"Text","text":"Checkout","variant":"h1"},{"id":"shippingInfoColumn","component":"Column","children":["nameField","addressField","cityField","zipCodeField"]},{"id":"nameField","component":"TextField","label":"Name","value":{"path":"/shipping/name"}},{"id":"addressField","component":"TextField","label":"Address","value":{"path":"/shipping/address"}},{"id":"cityField","component":"TextField","label":"City","value":{"path":"/shipping/city"}},{"id":"zipCodeField","component":"TextField","label":"Zip Code","value":{"path":"/shipping/zipCode"}},{"id":"paymentInfoColumn","component":"Column","children":["cardNumberField","expiryDateField","cvvField"]},{"id":"cardNumberField","component":"TextField","label":"Card Number","value":{"path":"/payment/cardNumber"}},{"id":"expiryDateField","component":"TextField","label":"Expiry Date","value":{"path":"/payment/expiryDate"}},{"id":"cvvField","component":"TextField","label":"CVV","value":{"path":"/payment/cvv"}},{"id":"totalText","component":"Text","text":"Total: $99.99"},{"id":"placeOrderButton","component":"Button","child":"placeOrderButtonText","action":{"event":{"name":"placeOrder"}}},{"id":"placeOrderButtonText","component":"Text","text":"Place Order"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"shipping":{"name":"","address":"","city":"","zipCode":""},"payment":{"cardNumber":"","expiryDate":"","cvv":""}}}} diff --git a/examples/catalog_gallery/samples/cinemaSeatSelection.sample b/examples/catalog_gallery/samples/cinemaSeatSelection.sample new file mode 100644 index 000000000..df7f9a30e --- /dev/null +++ b/examples/catalog_gallery/samples/cinemaSeatSelection.sample @@ -0,0 +1,12 @@ +--- +description: A seat selection grid. +name: cinemaSeatSelection +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for cinema seat selection. 'Text' (h1) "Select Seats". 'Text' "Screen" (centered). 'Column' of 'Row's representing rows of seats. + - Row A: 4 'CheckBox'es. + - Row B: 4 'CheckBox'es. + - Row C: 4 'CheckBox'es. + 'Button' "Confirm Selection". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["titleText","screenText","seatRowA","seatRowB","seatRowC","confirmButton"],"align":"center"},{"id":"titleText","component":"Text","text":"Select Seats","variant":"h1"},{"id":"screenText","component":"Text","text":"Screen","align":"center"},{"id":"seatRowA","component":"Row","children":["seatA1","seatA2","seatA3","seatA4"],"justify":"center"},{"id":"seatA1","component":"CheckBox","label":"A1","value":false},{"id":"seatA2","component":"CheckBox","label":"A2","value":false},{"id":"seatA3","component":"CheckBox","label":"A3","value":false},{"id":"seatA4","component":"CheckBox","label":"A4","value":false},{"id":"seatRowB","component":"Row","children":["seatB1","seatB2","seatB3","seatB4"],"justify":"center"},{"id":"seatB1","component":"CheckBox","label":"B1","value":false},{"id":"seatB2","component":"CheckBox","label":"B2","value":false},{"id":"seatB3","component":"CheckBox","label":"B3","value":false},{"id":"seatB4","component":"CheckBox","label":"B4","value":false},{"id":"seatRowC","component":"Row","children":["seatC1","seatC2","seatC3","seatC4"],"justify":"center"},{"id":"seatC1","component":"CheckBox","label":"C1","value":false},{"id":"seatC2","component":"CheckBox","label":"C2","value":false},{"id":"seatC3","component":"CheckBox","label":"C3","value":false},{"id":"seatC4","component":"CheckBox","label":"C4","value":false},{"id":"confirmButton","component":"Button","child":"confirmButtonText","action":{"event":{"name":"confirmSelection"}}},{"id":"confirmButtonText","component":"Text","text":"Confirm Selection"}]}} diff --git a/examples/catalog_gallery/samples/clientSideValidation.sample b/examples/catalog_gallery/samples/clientSideValidation.sample new file mode 100644 index 000000000..b39c503d9 --- /dev/null +++ b/examples/catalog_gallery/samples/clientSideValidation.sample @@ -0,0 +1,10 @@ +--- +description: A text field with client-side validation requirements. +name: clientSideValidation +prompt: | + Create a 'createSurface' and 'updateComponents' message for a registration form with validation. Surface ID 'main'. + Include a 'TextField' for "Username" that MUST match the regex "^[a-zA-Z0-9]{3,}$". If it fails, show error "Username must be at least 3 alphanumeric characters". + Include a 'Button' labeled "Register". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["usernameTextField","registerButton"]},{"id":"usernameTextField","component":"TextField","label":"Username","value":{"path":"/username"},"checks":[{"condition":{"call":"regex","args":{"value":{"path":"/username"},"pattern":"^[a-zA-Z0-9]{3,}$"}},"message":"Username must be at least 3 alphanumeric characters"}]},{"id":"registerButton","component":"Button","child":"registerButtonText","action":{"event":{"name":"registerUser"}}},{"id":"registerButtonText","component":"Text","text":"Register"}]}} diff --git a/examples/catalog_gallery/samples/contactCard.sample b/examples/catalog_gallery/samples/contactCard.sample new file mode 100644 index 000000000..2c519a176 --- /dev/null +++ b/examples/catalog_gallery/samples/contactCard.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display contact information. +name: contactCard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a contact card. The root component of the surface must be a 'Card'. This Card should contain a 'Row'. The row contains an 'Image' (as an avatar) and a 'Column'. The column contains a 'Text' for the name "Jane Doe", a 'Text' for the email "jane.doe@example.com", and a 'Text' for the phone number "(123) 456-7890". Below the main row, add a 'Button' labeled "View on Map" (using a child 'Text' component). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"mainColumn"},{"id":"mainColumn","component":"Column","children":["contactRow","mapButton"]},{"id":"contactRow","component":"Row","children":["avatarImage","contactDetailsColumn"],"align":"center"},{"id":"avatarImage","component":"Image","url":"https://example.com/avatar.jpg","variant":"avatar"},{"id":"contactDetailsColumn","component":"Column","children":["nameText","emailText","phoneText"]},{"id":"nameText","component":"Text","text":"Jane Doe","variant":"h5"},{"id":"emailText","component":"Text","text":"jane.doe@example.com","variant":"body"},{"id":"phoneText","component":"Text","text":"(123) 456-7890","variant":"body"},{"id":"mapButton","component":"Button","child":"mapButtonText","action":{"functionCall":{"call":"openUrl","args":{"url":"https://maps.google.com/?q=Jane+Doe+address"},"returnType":"void"}}},{"id":"mapButtonText","component":"Text","text":"View on Map"}]}} diff --git a/examples/catalog_gallery/samples/courseSyllabus.sample b/examples/catalog_gallery/samples/courseSyllabus.sample new file mode 100644 index 000000000..33cdb0c1d --- /dev/null +++ b/examples/catalog_gallery/samples/courseSyllabus.sample @@ -0,0 +1,10 @@ +--- +description: A course syllabus outline. +name: courseSyllabus +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a course syllabus. 'Text' (h1) "Introduction to Computer Science". 'List' of modules. + - For module 1, a 'Card' with 'Text' "Algorithms" and 'List' ("Sorting", "Searching"). + - For module 2, a 'Card' with 'Text' "Data Structures" and 'List' ("Arrays", "Linked Lists"). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["title","modulesList"]},{"id":"title","component":"Text","text":"Introduction to Computer Science","variant":"h1"},{"id":"modulesList","component":"List","children":["module1Card","module2Card"],"direction":"vertical"},{"id":"module1Card","component":"Card","child":"module1Content"},{"id":"module1Content","component":"Column","children":["module1Title","module1Items"]},{"id":"module1Title","component":"Text","text":"Algorithms","variant":"h4"},{"id":"module1Items","component":"List","children":["sortingText","searchingText"],"direction":"vertical"},{"id":"sortingText","component":"Text","text":"Sorting","variant":"body"},{"id":"searchingText","component":"Text","text":"Searching","variant":"body"},{"id":"module2Card","component":"Card","child":"module2Content"},{"id":"module2Content","component":"Column","children":["module2Title","module2Items"]},{"id":"module2Title","component":"Text","text":"Data Structures","variant":"h4"},{"id":"module2Items","component":"List","children":["arraysText","linkedListsText"],"direction":"vertical"},{"id":"arraysText","component":"Text","text":"Arrays","variant":"body"},{"id":"linkedListsText","component":"Text","text":"Linked Lists","variant":"body"}]}} diff --git a/examples/catalog_gallery/samples/dashboard.sample b/examples/catalog_gallery/samples/dashboard.sample new file mode 100644 index 000000000..e0595889e --- /dev/null +++ b/examples/catalog_gallery/samples/dashboard.sample @@ -0,0 +1,8 @@ +--- +description: A simple dashboard with statistics. +name: dashboard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a simple dashboard. It should have a 'Text' (variant 'h1') "Sales Dashboard". Below, a 'Row' containing three 'Card's. The first card has a 'Text' "Revenue" and another 'Text' "$50,000". The second card has "New Customers" and "1,200". The third card has "Conversion Rate" and "4.5%". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","dashboardCardsRow"]},{"id":"dashboardTitle","component":"Text","text":"Sales Dashboard","variant":"h1"},{"id":"dashboardCardsRow","component":"Row","children":["revenueCard","customersCard","conversionCard"],"justify":"spaceBetween"},{"id":"revenueCard","component":"Card","child":"revenueCardContent"},{"id":"revenueCardContent","component":"Column","children":["revenueLabel","revenueValue"]},{"id":"revenueLabel","component":"Text","text":"Revenue"},{"id":"revenueValue","component":"Text","text":"$50,000","variant":"h3"},{"id":"customersCard","component":"Card","child":"customersCardContent"},{"id":"customersCardContent","component":"Column","children":["customersLabel","customersValue"]},{"id":"customersLabel","component":"Text","text":"New Customers"},{"id":"customersValue","component":"Text","text":"1,200","variant":"h3"},{"id":"conversionCard","component":"Card","child":"conversionCardContent"},{"id":"conversionCardContent","component":"Column","children":["conversionLabel","conversionValue"]},{"id":"conversionLabel","component":"Text","text":"Conversion Rate"},{"id":"conversionValue","component":"Text","text":"4.5%","variant":"h3"}]}} diff --git a/examples/catalog_gallery/samples/dogBreedGenerator.sample b/examples/catalog_gallery/samples/dogBreedGenerator.sample new file mode 100644 index 000000000..79fbe9fee --- /dev/null +++ b/examples/catalog_gallery/samples/dogBreedGenerator.sample @@ -0,0 +1,26 @@ +--- +description: A prompt to generate a UI for a dog breed information and generator tool. +name: dogBreedGenerator +prompt: | + Use a surfaceId of 'main'. Then, generate a 'createSurface' message followed by 'updateComponents' message to describe the following UI: + + A vertical list with: + - Dog breed information + - Dog generator + + The dog breed information is a card, which contains a title “Famous Dog breeds”, a header image, and a horizontal list of images of different dog breeds (using a 'List' component). The list information should be in the data model at /breeds. + + The dog generator is another card which is a form that generates a fictional dog breed with a description + - Title + - Description text explaining what it is + - Dog breed name (text input) + - Number of legs (number input) + - Button called “Generate” which takes the data above and generates a new dog description + - Skills (ChoicePicker component, variant 'multipleSelection') + - A divider + - A section which shows the generated content + +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"dogBreedName":"","numberOfLegs":0,"selectedSkills":[],"generatedDescription":"","breeds":[{"url":"https://upload.wikimedia.org/wikipedia/commons/a/a2/National_geographic_dog_on_fence.jpeg"},{"url":"https://upload.wikimedia.org/wikipedia/commons/e/e0/Dog_training.jpeg"},{"url":"https://upload.wikimedia.org/wikipedia/commons/a/a5/Red_Kangaroo_-_Taronga_Zoo.jpg"}],"allSkills":[{"label":"Jumping","value":"jumping"},{"label":"Swimming","value":"swimming"},{"label":"Running","value":"running"},{"label":"Flying","value":"flying"}]}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dogInfoCard","dogGeneratorCard"]},{"id":"dogInfoCard","component":"Card","child":"dogInfoColumn"},{"id":"dogInfoColumn","component":"Column","children":["dogInfoTitle","headerImage","dogBreedList"]},{"id":"dogInfoTitle","component":"Text","text":"Famous Dog breeds","variant":"h3"},{"id":"headerImage","component":"Image","url":"https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Golden_retriever_for_service.jpg/1024px-Golden_retriever_for_service.jpg","variant":"header"},{"id":"dogBreedList","component":"List","direction":"horizontal","children":{"componentId":"breedImage","path":"/breeds"}},{"id":"breedImage","component":"Image","url":{"path":"/url"},"variant":"avatar"},{"id":"dogGeneratorCard","component":"Card","child":"dogGeneratorColumn"},{"id":"dogGeneratorColumn","component":"Column","children":["generatorTitle","generatorDescriptionText","dogBreedNameInput","numberOfLegsInput","skillsChoicePicker","generateButton","divider","generatedContent"]},{"id":"generatorTitle","component":"Text","text":"Dog Generator","variant":"h3"},{"id":"generatorDescriptionText","component":"Text","text":"Generate a fictional dog breed with a description.","variant":"body"},{"id":"dogBreedNameInput","component":"TextField","label":"Dog breed name","value":{"path":"/dogBreedName"}},{"id":"numberOfLegsInput","component":"TextField","label":"Number of legs","variant":"number","value":{"path":"/numberOfLegs"}},{"id":"skillsChoicePicker","component":"ChoicePicker","label":"Skills","variant":"multipleSelection","options":{"path":"/allSkills"},"value":{"path":"/selectedSkills"}},{"id":"generateButton","component":"Button","child":"generateButtonText","action":{"event":{"name":"generateDogDescription","context":{"breedName":{"path":"/dogBreedName"},"legs":{"path":"/numberOfLegs"},"skills":{"path":"/selectedSkills"}}}}},{"id":"generateButtonText","component":"Text","text":"Generate"},{"id":"divider","component":"Divider","axis":"horizontal"},{"id":"generatedContent","component":"Column","children":["generatedContentTitle","generatedDescriptionText"]},{"id":"generatedContentTitle","component":"Text","text":"Generated Dog Description:","variant":"h4"},{"id":"generatedDescriptionText","component":"Text","text":{"path":"/generatedDescription"},"variant":"body"}]}} diff --git a/examples/catalog_gallery/samples/eCommerceProductPage.sample b/examples/catalog_gallery/samples/eCommerceProductPage.sample new file mode 100644 index 000000000..00495a5be --- /dev/null +++ b/examples/catalog_gallery/samples/eCommerceProductPage.sample @@ -0,0 +1,18 @@ +--- +description: A detailed product page for an e-commerce website. +name: eCommerceProductPage +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a product details page. + The main layout should be a 'Row'. + The left side of the row is a 'Column' containing a large main 'Image' of the product, and below it, a 'Row' of three smaller thumbnail 'Image' components. + The right side of the row is another 'Column' for product information: + - A 'Text' (variant 'h1') for the product name, "Premium Leather Jacket". + - A 'Text' component for the price, "$299.99". + - A 'Divider'. + - A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Select Size" with options "S", "M", "L", "XL". + - A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Select Color" with options "Black", "Brown", "Red". + - A 'Button' with a 'Text' child "Add to Cart". + - A 'Text' component for the product description below the button. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Row","children":["productImageColumn","productInfoColumn"]},{"id":"productImageColumn","component":"Column","weight":1,"children":["mainProductImage","thumbnailImagesRow"]},{"id":"mainProductImage","component":"Image","url":"https://example.com/leather-jacket-main.jpg","variant":"largeFeature"},{"id":"thumbnailImagesRow","component":"Row","children":["thumbnailImage1","thumbnailImage2","thumbnailImage3"]},{"id":"thumbnailImage1","component":"Image","url":"https://example.com/leather-jacket-thumb1.jpg","variant":"smallFeature"},{"id":"thumbnailImage2","component":"Image","url":"https://example.com/leather-jacket-thumb2.jpg","variant":"smallFeature"},{"id":"thumbnailImage3","component":"Image","url":"https://example.com/leather-jacket-thumb3.jpg","variant":"smallFeature"},{"id":"productInfoColumn","component":"Column","weight":1,"children":["productName","productPrice","infoDivider","sizePicker","colorPicker","addToCartButton","addToCartButtonText","productDescription"]},{"id":"productName","component":"Text","text":"Premium Leather Jacket","variant":"h1"},{"id":"productPrice","component":"Text","text":"$299.99"},{"id":"infoDivider","component":"Divider"},{"id":"sizePicker","component":"ChoicePicker","label":"Select Size","variant":"mutuallyExclusive","options":[{"label":"S","value":"small"},{"label":"M","value":"medium"},{"label":"L","value":"large"},{"label":"XL","value":"extraLarge"}],"value":{"path":"/selectedSize"}},{"id":"colorPicker","component":"ChoicePicker","label":"Select Color","variant":"mutuallyExclusive","options":[{"label":"Black","value":"black"},{"label":"Brown","value":"brown"},{"label":"Red","value":"red"}],"value":{"path":"/selectedColor"}},{"id":"addToCartButton","component":"Button","child":"addToCartButtonText","action":{"event":{"name":"addToCart","context":{"productId":"jacket123","size":{"path":"/selectedSize"},"color":{"path":"/selectedColor"}}}}},{"id":"addToCartButtonText","component":"Text","text":"Add to Cart"},{"id":"productDescription","component":"Text","text":"Crafted from genuine leather, this jacket offers timeless style and exceptional durability. Perfect for any season."}]}} diff --git a/examples/catalog_gallery/samples/fileBrowser.sample b/examples/catalog_gallery/samples/fileBrowser.sample new file mode 100644 index 000000000..89dcc1339 --- /dev/null +++ b/examples/catalog_gallery/samples/fileBrowser.sample @@ -0,0 +1,8 @@ +--- +description: A file explorer list. +name: fileBrowser +prompt: | + Create a file browser. It should have a 'Text' (variant 'h1') "My Files". A 'List' of 'Row's. Each row has an 'Icon' (folder or attachFile) and a 'Text' (filename). Examples (create these as static rows, not data bound): "Documents", "Images", "Work.txt". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["myFilesTitle","fileList"]},{"id":"myFilesTitle","component":"Text","text":"My Files","variant":"h1"},{"id":"fileList","component":"List","direction":"vertical","children":["row1","row2","row3"]},{"id":"row1","component":"Row","children":["iconFolder1","textDocuments"]},{"id":"iconFolder1","component":"Icon","name":"folder"},{"id":"textDocuments","component":"Text","text":"Documents"},{"id":"row2","component":"Row","children":["iconFolder2","textImages"]},{"id":"iconFolder2","component":"Icon","name":"folder"},{"id":"textImages","component":"Text","text":"Images"},{"id":"row3","component":"Row","children":["iconFile3","textWorkTxt"]},{"id":"iconFile3","component":"Icon","name":"attachFile"},{"id":"textWorkTxt","component":"Text","text":"Work.txt"}]}} diff --git a/examples/catalog_gallery/samples/fitnessTracker.sample b/examples/catalog_gallery/samples/fitnessTracker.sample new file mode 100644 index 000000000..aa9e8809a --- /dev/null +++ b/examples/catalog_gallery/samples/fitnessTracker.sample @@ -0,0 +1,9 @@ +--- +description: A daily activity summary. +name: fitnessTracker +prompt: | + Create a fitness tracker dashboard. It should have a 'Text' (variant 'h1') "Daily Activity", and a 'Row' of 'Card's. Each card should contain a 'Column' with a 'Text' label (e.g. "Steps") and a 'Text' value (e.g. "10,000"). Create cards for "Steps" ("10,000"), "Calories" ("500 kcal"), "Distance" ("5 km"). Below that, a 'Slider' labeled "Daily Goal" (initialize value to 50). Finally, a 'List' of recent workouts. Use 'Text' components for the list items, for example: "Morning Run", "Evening Yoga", "Gym Session". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dailyActivityTitle","activityCardsRow","dailyGoalSlider","workoutsList"]},{"id":"dailyActivityTitle","component":"Text","text":"Daily Activity","variant":"h1"},{"id":"activityCardsRow","component":"Row","children":["stepsCard","caloriesCard","distanceCard"],"justify":"spaceBetween"},{"id":"stepsCard","component":"Card","child":"stepsColumn"},{"id":"stepsColumn","component":"Column","children":["stepsLabel","stepsValue"]},{"id":"stepsLabel","component":"Text","text":"Steps"},{"id":"stepsValue","component":"Text","text":"10,000"},{"id":"caloriesCard","component":"Card","child":"caloriesColumn"},{"id":"caloriesColumn","component":"Column","children":["caloriesLabel","caloriesValue"]},{"id":"caloriesLabel","component":"Text","text":"Calories"},{"id":"caloriesValue","component":"Text","text":"500 kcal"},{"id":"distanceCard","component":"Card","child":"distanceColumn"},{"id":"distanceColumn","component":"Column","children":["distanceLabel","distanceValue"]},{"id":"distanceLabel","component":"Text","text":"Distance"},{"id":"distanceValue","component":"Text","text":"5 km"},{"id":"dailyGoalSlider","component":"Slider","label":"Daily Goal","min":0,"max":100,"value":{"path":"/dailyGoal"}},{"id":"workoutsList","component":"List","children":["workout1","workout2","workout3"],"direction":"vertical"},{"id":"workout1","component":"Text","text":"Morning Run"},{"id":"workout2","component":"Text","text":"Evening Yoga"},{"id":"workout3","component":"Text","text":"Gym Session"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"dailyGoal":50}}} diff --git a/examples/catalog_gallery/samples/flashcardApp.sample b/examples/catalog_gallery/samples/flashcardApp.sample new file mode 100644 index 000000000..de01ebbc0 --- /dev/null +++ b/examples/catalog_gallery/samples/flashcardApp.sample @@ -0,0 +1,8 @@ +--- +description: A language learning flashcard. +name: flashcardApp +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a flashcard app. 'Text' (h1) "Spanish Vocabulary". 'Card' (the flashcard). Inside the card, a 'Column' with 'Text' (h2) "Hola" (Front). 'Divider'. 'Text' "Hello" (Back - conceptually hidden, but rendered here). 'Row' of buttons: "Hard", "Good", "Easy". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["appTitle","flashcard","buttonsRow"],"justify":"center","align":"center"},{"id":"appTitle","component":"Text","text":"Spanish Vocabulary","variant":"h1"},{"id":"flashcard","component":"Card","child":"flashcardContent"},{"id":"flashcardContent","component":"Column","children":["flashcardFront","flashcardDivider","flashcardBack"],"align":"center"},{"id":"flashcardFront","component":"Text","text":"Hola","variant":"h2"},{"id":"flashcardDivider","component":"Divider","axis":"horizontal"},{"id":"flashcardBack","component":"Text","text":"Hello"},{"id":"buttonsRow","component":"Row","children":["hardButton","goodButton","easyButton"],"justify":"spaceBetween"},{"id":"hardButton","component":"Button","child":"hardButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"hard"}}}},{"id":"hardButtonText","component":"Text","text":"Hard"},{"id":"goodButton","component":"Button","child":"goodButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"good"}}}},{"id":"goodButtonText","component":"Text","text":"Good"},{"id":"easyButton","component":"Button","child":"easyButtonText","action":{"event":{"name":"rateFlashcard","context":{"rating":"easy"}}}},{"id":"easyButtonText","component":"Text","text":"Easy"}]}} diff --git a/examples/catalog_gallery/samples/flightBooker.sample b/examples/catalog_gallery/samples/flightBooker.sample new file mode 100644 index 000000000..5d3dd2078 --- /dev/null +++ b/examples/catalog_gallery/samples/flightBooker.sample @@ -0,0 +1,9 @@ +--- +description: A form to search for flights. +name: flightBooker +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a flight booking form. It should have a 'Text' (variant 'h1') "Book a Flight". Then a 'Row' with two 'TextField's for "Origin" and "Destination". Below that, a 'Row' with two 'DateTimeInput's for "Departure Date" and "Return Date" (initialize with empty values). Add a 'Slider' labeled "Passengers" (min 1, max 10, value 1). Finally, a 'Button' labeled "Search Flights". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["bookFlightTitle","originDestinationRow","datesRow","passengersSlider","searchFlightsButton"]},{"id":"bookFlightTitle","component":"Text","text":"Book a Flight","variant":"h1"},{"id":"originDestinationRow","component":"Row","children":["originTextField","destinationTextField"]},{"id":"originTextField","component":"TextField","label":"Origin","value":{"path":"/origin"}},{"id":"destinationTextField","component":"TextField","label":"Destination","value":{"path":"/destination"}},{"id":"datesRow","component":"Row","children":["departureDateInput","returnDateInput"]},{"id":"departureDateInput","component":"DateTimeInput","label":"Departure Date","value":{"path":"/departureDate"},"enableDate":true},{"id":"returnDateInput","component":"DateTimeInput","label":"Return Date","value":{"path":"/returnDate"},"enableDate":true},{"id":"passengersSlider","component":"Slider","label":"Passengers","min":1,"max":10,"value":{"path":"/passengers"}},{"id":"searchFlightsButton","component":"Button","child":"searchFlightsButtonText","action":{"event":{"name":"searchFlights","context":{"origin":{"path":"/origin"},"destination":{"path":"/destination"},"departureDate":{"path":"/departureDate"},"returnDate":{"path":"/returnDate"},"passengers":{"path":"/passengers"}}}}},{"id":"searchFlightsButtonText","component":"Text","text":"Search Flights"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"origin":"","destination":"","departureDate":"","returnDate":"","passengers":1}}} diff --git a/examples/catalog_gallery/samples/hello_world.sample b/examples/catalog_gallery/samples/hello_world.sample index 16cd345cb..5d7b80d53 100644 --- a/examples/catalog_gallery/samples/hello_world.sample +++ b/examples/catalog_gallery/samples/hello_world.sample @@ -1,5 +1,5 @@ name: Test Sample description: This is a test sample to verify the parser. --- -{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello World"}}}}]}} -{"beginRendering": {"surfaceId": "default", "root": "text1"}} +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Text","text":"Hello World!","variant":"h1"}]}} diff --git a/examples/catalog_gallery/samples/hotelSearchResults.sample b/examples/catalog_gallery/samples/hotelSearchResults.sample new file mode 100644 index 000000000..80b2900ca --- /dev/null +++ b/examples/catalog_gallery/samples/hotelSearchResults.sample @@ -0,0 +1,10 @@ +--- +description: Hotel search results list. +name: hotelSearchResults +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for hotel search results. 'Text' (h1) "Hotels in Tokyo". 'List' of 'Card's. + - Card 1: 'Row' with 'Image', 'Column' ('Text' "Grand Hotel", 'Text' "5 Stars", 'Text' "$200/night"), 'Button' "Book". + - Card 2: 'Row' with 'Image', 'Column' ('Text' "City Inn", 'Text' "3 Stars", 'Text' "$100/night"), 'Button' "Book". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["hotelsTitle","hotelList"]},{"id":"hotelsTitle","component":"Text","text":"Hotels in Tokyo","variant":"h1"},{"id":"hotelList","component":"List","direction":"vertical","children":["card1","card2"]},{"id":"card1","component":"Card","child":"card1Content"},{"id":"card1Content","component":"Row","children":["card1Image","card1Details","card1Button"]},{"id":"card1Image","component":"Image","url":"https://example.com/grand-hotel.jpg","variant":"smallFeature"},{"id":"card1Details","component":"Column","children":["card1Name","card1Stars","card1Price"]},{"id":"card1Name","component":"Text","text":"Grand Hotel"},{"id":"card1Stars","component":"Text","text":"5 Stars"},{"id":"card1Price","component":"Text","text":"$200/night"},{"id":"card1Button","component":"Button","child":"card1ButtonText","action":{"event":{"name":"bookHotel","context":{"hotelId":"grandHotel"}}}},{"id":"card1ButtonText","component":"Text","text":"Book"},{"id":"card2","component":"Card","child":"card2Content"},{"id":"card2Content","component":"Row","children":["card2Image","card2Details","card2Button"]},{"id":"card2Image","component":"Image","url":"https://example.com/city-inn.jpg","variant":"smallFeature"},{"id":"card2Details","component":"Column","children":["card2Name","card2Stars","card2Price"]},{"id":"card2Name","component":"Text","text":"City Inn"},{"id":"card2Stars","component":"Text","text":"3 Stars"},{"id":"card2Price","component":"Text","text":"$100/night"},{"id":"card2Button","component":"Button","child":"card2ButtonText","action":{"event":{"name":"bookHotel","context":{"hotelId":"cityInn"}}}},{"id":"card2ButtonText","component":"Text","text":"Book"}]}} diff --git a/examples/catalog_gallery/samples/interactiveDashboard.sample b/examples/catalog_gallery/samples/interactiveDashboard.sample new file mode 100644 index 000000000..5e4b3568e --- /dev/null +++ b/examples/catalog_gallery/samples/interactiveDashboard.sample @@ -0,0 +1,17 @@ +--- +description: A dashboard with filters and data cards. +name: interactiveDashboard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for an interactive analytics dashboard. + At the top, a 'Text' (variant 'h1') "Company Dashboard". + Below the text heading, a 'Card' containing a 'Row' of filter controls: + - A 'DateTimeInput' with a label for "Start Date" (initialize with empty value). + - A 'DateTimeInput' with a label for "End Date" (initialize with empty value). + - A 'Button' labeled "Apply Filters". + Below the filters card, a 'Row' containing two 'Card's for key metrics: + - The first 'Card' has a 'Text' (variant 'h2') "Total Revenue" and a 'Text' component showing "$1,234,567". + - The second 'Card' has a 'Text' (variant 'h2') "New Users" and a 'Text' component showing "4,321". + Finally, a large 'Card' at the bottom with a 'Text' (variant 'h2') "Revenue Over Time" and a placeholder 'Image' with a valid URL to represent a line chart. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","filtersCard","metricsRow","revenueChartCard"]},{"id":"dashboardTitle","component":"Text","text":"Company Dashboard","variant":"h1"},{"id":"filtersCard","component":"Card","child":"filterControlsRow"},{"id":"filterControlsRow","component":"Row","children":["startDateInput","endDateInput","applyFiltersButton"],"justify":"spaceBetween"},{"id":"startDateInput","component":"DateTimeInput","label":"Start Date","value":"","enableDate":true},{"id":"endDateInput","component":"DateTimeInput","label":"End Date","value":"","enableDate":true},{"id":"applyFiltersButton","component":"Button","child":"applyFiltersText","action":{"event":{"name":"applyFilters"}}},{"id":"applyFiltersText","component":"Text","text":"Apply Filters"},{"id":"metricsRow","component":"Row","children":["totalRevenueCard","newUsersCard"],"justify":"spaceEvenly"},{"id":"totalRevenueCard","component":"Card","child":"totalRevenueColumn"},{"id":"totalRevenueColumn","component":"Column","children":["totalRevenueTitle","totalRevenueValue"]},{"id":"totalRevenueTitle","component":"Text","text":"Total Revenue","variant":"h2"},{"id":"totalRevenueValue","component":"Text","text":"$1,234,567"},{"id":"newUsersCard","component":"Card","child":"newUsersColumn"},{"id":"newUsersColumn","component":"Column","children":["newUsersTitle","newUsersValue"]},{"id":"newUsersTitle","component":"Text","text":"New Users","variant":"h2"},{"id":"newUsersValue","component":"Text","text":"4,321"},{"id":"revenueChartCard","component":"Card","child":"revenueChartColumn"},{"id":"revenueChartColumn","component":"Column","children":["revenueChartTitle","revenueChartImage"]},{"id":"revenueChartTitle","component":"Text","text":"Revenue Over Time","variant":"h2"},{"id":"revenueChartImage","component":"Image","url":"https://via.placeholder.com/600x300?text=Line+Chart"}]}} diff --git a/examples/catalog_gallery/samples/jobApplication.sample b/examples/catalog_gallery/samples/jobApplication.sample new file mode 100644 index 000000000..78f1cda53 --- /dev/null +++ b/examples/catalog_gallery/samples/jobApplication.sample @@ -0,0 +1,9 @@ +--- +description: A job application form. +name: jobApplication +prompt: | + Create a job application form. It should have 'TextField's for "Name", "Email", "Phone", "Resume URL". A 'ChoicePicker' (variant 'mutuallyExclusive') labeled "Years of Experience" (options: "0-1", "2-5", "5+"). A 'Button' "Submit Application". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"name":"","email":"","phone":"","resumeUrl":"","experience":""}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["nameField","emailField","phoneField","resumeUrlField","experiencePicker","submitButton"],"justify":"start","align":"stretch"},{"id":"nameField","component":"TextField","label":"Name","value":{"path":"/name"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/name"}}},"message":"Name is required."}]},{"id":"emailField","component":"TextField","label":"Email","value":{"path":"/email"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/email"}}},"message":"Email is required."},{"condition":{"call":"email","args":{"value":{"path":"/email"}}},"message":"Invalid email format."}]},{"id":"phoneField","component":"TextField","label":"Phone","value":{"path":"/phone"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/phone"}}},"message":"Phone number is required."},{"condition":{"call":"regex","args":{"value":{"path":"/phone"},"pattern":"^\\+?[1-9]\\d{1,14}$"}},"message":"Invalid phone number format."}]},{"id":"resumeUrlField","component":"TextField","label":"Resume URL","value":{"path":"/resumeUrl"},"checks":[{"condition":{"call":"required","args":{"value":{"path":"/resumeUrl"}}},"message":"Resume URL is required."}]},{"id":"experiencePicker","component":"ChoicePicker","label":"Years of Experience","variant":"mutuallyExclusive","options":[{"label":"0-1","value":"0-1"},{"label":"2-5","value":"2-5"},{"label":"5+","value":"5+"}],"value":{"path":"/experience"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitApplication","context":{"name":{"path":"/name"},"email":{"path":"/email"},"phone":{"path":"/phone"},"resumeUrl":{"path":"/resumeUrl"},"experience":{"path":"/experience"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Application"}]}} diff --git a/examples/catalog_gallery/samples/kanbanBoard.sample b/examples/catalog_gallery/samples/kanbanBoard.sample new file mode 100644 index 000000000..0ed4fc85a --- /dev/null +++ b/examples/catalog_gallery/samples/kanbanBoard.sample @@ -0,0 +1,12 @@ +--- +description: A Kanban-style task tracking board. +name: kanbanBoard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a Kanban board. It should have a 'Text' (variant 'h1') "Project Tasks". Below, a 'Row' containing three 'Column's representing "To Do", "In Progress", and "Done". Each column should have a 'Text' (variant 'h2') header and a list of 'Card's. + - "To Do" column: Card "Research", Card "Design". + - "In Progress" column: Card "Implementation". + - "Done" column: Card "Planning". + Each card should just contain a 'Text' with the task name. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["projectTitle","kanbanBoard"]},{"id":"projectTitle","component":"Text","text":"Project Tasks","variant":"h1"},{"id":"kanbanBoard","component":"Row","children":["toDoColumn","inProgressColumn","doneColumn"],"justify":"spaceEvenly"},{"id":"toDoColumn","component":"Column","children":["toDoHeader","cardResearch","cardDesign"],"align":"start"},{"id":"toDoHeader","component":"Text","text":"To Do","variant":"h2"},{"id":"cardResearch","component":"Card","child":"textResearch"},{"id":"textResearch","component":"Text","text":"Research"},{"id":"cardDesign","component":"Card","child":"textDesign"},{"id":"textDesign","component":"Text","text":"Design"},{"id":"inProgressColumn","component":"Column","children":["inProgressHeader","cardImplementation"],"align":"start"},{"id":"inProgressHeader","component":"Text","text":"In Progress","variant":"h2"},{"id":"cardImplementation","component":"Card","child":"textImplementation"},{"id":"textImplementation","component":"Text","text":"Implementation"},{"id":"doneColumn","component":"Column","children":["doneHeader","cardPlanning"],"align":"start"},{"id":"doneHeader","component":"Text","text":"Done","variant":"h2"},{"id":"cardPlanning","component":"Card","child":"textPlanning"},{"id":"textPlanning","component":"Text","text":"Planning"}]}} diff --git a/examples/catalog_gallery/samples/loginForm.sample b/examples/catalog_gallery/samples/loginForm.sample new file mode 100644 index 000000000..6da71b302 --- /dev/null +++ b/examples/catalog_gallery/samples/loginForm.sample @@ -0,0 +1,8 @@ +--- +description: A simple login form with username, password, a "remember me" checkbox, and a submit button. +name: loginForm +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a login form. It should have a "Login" text (variant 'h1'), two text fields for username and password (bound to /login/username and /login/password), a checkbox for "Remember Me" (bound to /login/rememberMe), and a "Sign In" button. The button's action should have a 'event' property with 'name': 'login', and a 'context' containing the username, password, and rememberMe status. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["loginTitle","usernameField","passwordField","rememberMeCheckbox","signInButton"],"justify":"center","align":"center"},{"id":"loginTitle","component":"Text","text":"Login","variant":"h1"},{"id":"usernameField","component":"TextField","label":"Username","value":{"path":"/login/username"}},{"id":"passwordField","component":"TextField","label":"Password","value":{"path":"/login/password"},"variant":"obscured"},{"id":"rememberMeCheckbox","component":"CheckBox","label":"Remember Me","value":{"path":"/login/rememberMe"}},{"id":"signInButton","component":"Button","child":"signInButtonText","action":{"event":{"name":"login","context":{"username":{"path":"/login/username"},"password":{"path":"/login/password"},"rememberMe":{"path":"/login/rememberMe"}}}}},{"id":"signInButtonText","component":"Text","text":"Sign In"}]}} diff --git a/examples/catalog_gallery/samples/musicPlayer.sample b/examples/catalog_gallery/samples/musicPlayer.sample new file mode 100644 index 000000000..db1a53b44 --- /dev/null +++ b/examples/catalog_gallery/samples/musicPlayer.sample @@ -0,0 +1,8 @@ +--- +description: A simple music player UI. +name: musicPlayer +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a music player. It should be a 'Card' containing a 'Column'. Inside the column, there's an 'Image' for the album art, a 'Text' for the song title "Bohemian Rhapsody", another 'Text' for the artist "Queen", a 'Slider' labeled "Progress", and a 'Row' with three 'Button' components. Each Button should have a child 'Text' component. The Text components should have the labels "Previous", "Play", and "Next" respectively. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"musicPlayerColumn"},{"id":"musicPlayerColumn","component":"Column","children":["albumArtImage","songTitleText","artistText","progressSlider","controlsRow"]},{"id":"albumArtImage","component":"Image","url":"https://upload.wikimedia.org/wikipedia/en/thumb/9/91/Queen_Bohemian_Rhapsody.png/220px-Queen_Bohemian_Rhapsody.png","variant":"mediumFeature"},{"id":"songTitleText","component":"Text","text":"Bohemian Rhapsody","variant":"h4"},{"id":"artistText","component":"Text","text":"Queen","variant":"body"},{"id":"progressSlider","component":"Slider","label":"Progress","min":0,"max":100,"value":50},{"id":"controlsRow","component":"Row","justify":"spaceAround","children":["previousButton","playButton","nextButton"]},{"id":"previousButton","component":"Button","child":"previousButtonText","action":{"event":{"name":"previousSong"}}},{"id":"previousButtonText","component":"Text","text":"Previous"},{"id":"playButton","component":"Button","child":"playButtonText","action":{"event":{"name":"playPause"}}},{"id":"playButtonText","component":"Text","text":"Play"},{"id":"nextButton","component":"Button","child":"nextButtonText","action":{"event":{"name":"nextSong"}}},{"id":"nextButtonText","component":"Text","text":"Next"}]}} diff --git a/examples/catalog_gallery/samples/nestedDataBinding.sample b/examples/catalog_gallery/samples/nestedDataBinding.sample new file mode 100644 index 000000000..c0d54face --- /dev/null +++ b/examples/catalog_gallery/samples/nestedDataBinding.sample @@ -0,0 +1,33 @@ +--- +description: A project dashboard with deeply nested data binding. +name: nestedDataBinding +prompt: | + Generate a stream of JSON messages for a Project Management Dashboard. + The output must consist of exactly three JSON objects, one after the other. + + Generate a createSurface message with surfaceId 'main'. + Generate an updateComponents message with surfaceId 'main'. + It should have a 'Text' (variant 'h1') "Project Dashboard". + Then a 'List' of projects bound to '/projects'. + Inside the list template, each item should be a 'Card' containing: + - A 'Text' (variant 'h2') bound to the project 'title'. + - A 'List' of tasks bound to the 'tasks' property of the project. + Inside the tasks list template, each item should be a 'Column' containing: + - A 'Text' bound to the task 'description'. + - A 'Row' for the assignee, containing: + - A 'Text' bound to 'assignee/name'. + - A 'Text' bound to 'assignee/role'. + - A 'List' of subtasks bound to 'subtasks'. + Inside the subtasks list template, each item should be a 'Text' bound to 'title'. + + Then generate an 'updateDataModel' message. + Populate this dashboard with sample data: + - At least one project. + - The project should have a title, and a list of tasks. + - The task should have a description, an assignee object (with name and role), and a list of subtasks. + + Ensure all referenced component IDs (like 'subtaskList') are explicitly defined in the 'components' list. The component with id 'subtaskList' must effectively exist in the output list. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["dashboardTitle","projectList"]},{"id":"dashboardTitle","component":"Text","variant":"h1","text":"Project Dashboard"},{"id":"projectList","component":"List","direction":"vertical","children":{"componentId":"projectCardTemplate","path":"/projects"}},{"id":"projectCardTemplate","component":"Card","child":"projectCardContent"},{"id":"projectCardContent","component":"Column","children":["projectTitle","taskList"]},{"id":"projectTitle","component":"Text","variant":"h2","text":{"path":"title"}},{"id":"taskList","component":"List","direction":"vertical","children":{"componentId":"taskItemTemplate","path":"tasks"}},{"id":"taskItemTemplate","component":"Column","children":["taskDescription","assigneeRow","subtaskList"]},{"id":"taskDescription","component":"Text","text":{"path":"description"}},{"id":"assigneeRow","component":"Row","children":["assigneeName","assigneeRole"]},{"id":"assigneeName","component":"Text","text":{"path":"assignee/name"}},{"id":"assigneeRole","component":"Text","text":{"path":"assignee/role"}},{"id":"subtaskList","component":"List","direction":"vertical","children":{"componentId":"subtaskItemTemplate","path":"subtasks"}},{"id":"subtaskItemTemplate","component":"Text","text":{"path":"title"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"projects":[{"title":"Website Redesign","tasks":[{"description":"Design mockups for homepage","assignee":{"name":"Alice Smith","role":"UI Designer"},"subtasks":[{"title":"Gather requirements"},{"title":"Sketch wireframes"},{"title":"Create high-fidelity designs"}]},{"description":"Develop user authentication module","assignee":{"name":"Bob Johnson","role":"Backend Developer"},"subtasks":[{"title":"Set up database"},{"title":"Implement login API"},{"title":"Integrate with frontend"}]}]},{"title":"Mobile App Development","tasks":[{"description":"Plan app features","assignee":{"name":"Charlie Brown","role":"Product Manager"},"subtasks":[{"title":"Market research"},{"title":"User stories"}]}]}]}}} diff --git a/examples/catalog_gallery/samples/nestedLayoutRecursive.sample b/examples/catalog_gallery/samples/nestedLayoutRecursive.sample new file mode 100644 index 000000000..b1e24f8bb --- /dev/null +++ b/examples/catalog_gallery/samples/nestedLayoutRecursive.sample @@ -0,0 +1,17 @@ +--- +description: A deeply nested layout to test component recursion. +name: nestedLayoutRecursive +prompt: | + Create a 'createSurface' and 'updateComponents' message with surfaceId 'main'. + Create a layout with at least 5 levels of depth: + Level 1: Card + Level 2: Column (inside Card) + Level 3: Row (inside Column) + Level 4: List (inside Row) + Level 5: Text (inside List items) + + Use explicit, static components for this structure (no data binding for the list). + Level 5 Text should say "Deep content". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"column1"},{"id":"column1","component":"Column","children":["row1"]},{"id":"row1","component":"Row","children":["list1"]},{"id":"list1","component":"List","children":["text1"],"direction":"vertical"},{"id":"text1","component":"Text","text":"Deep content"}]}} diff --git a/examples/catalog_gallery/samples/newsAggregator.sample b/examples/catalog_gallery/samples/newsAggregator.sample new file mode 100644 index 000000000..13e07c3b8 --- /dev/null +++ b/examples/catalog_gallery/samples/newsAggregator.sample @@ -0,0 +1,8 @@ +--- +description: A news feed with article cards. +name: newsAggregator +prompt: | + Create a news aggregator. The root component should be a 'Column'. Inside this column, place a 'Text' (variant 'h1') "Top Headlines". Below the text, place a 'List' of 'Card's. The 'List' should be a sibling of the 'Text', not a parent. Each card has a 'Column' with an 'Image', a 'Text' (headline), and a 'Text' (summary). Include headlines "Tech Breakthrough" and "Local Sports". Each card should have a 'Button' labeled "Read More". Create these as static components, not data bound. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["headlineText","newsList"]},{"id":"headlineText","component":"Text","text":"Top Headlines","variant":"h1"},{"id":"newsList","component":"List","direction":"vertical","children":["card1","card2"]},{"id":"card1","component":"Card","child":"card1Content"},{"id":"card1Content","component":"Column","children":["image1","headline1","summary1","button1"]},{"id":"image1","component":"Image","url":"https://picsum.photos/id/237/200/100"},{"id":"headline1","component":"Text","text":"Tech Breakthrough"},{"id":"summary1","component":"Text","text":"Scientists announce a major leap forward in AI."},{"id":"button1","component":"Button","child":"button1Label","action":{"event":{"name":"readMore","context":{"articleId":"tech-breakthrough"}}}},{"id":"button1Label","component":"Text","text":"Read More"},{"id":"card2","component":"Card","child":"card2Content"},{"id":"card2Content","component":"Column","children":["image2","headline2","summary2","button2"]},{"id":"image2","component":"Image","url":"https://picsum.photos/id/238/200/100"},{"id":"headline2","component":"Text","text":"Local Sports"},{"id":"summary2","component":"Text","text":"High school team wins championship title."},{"id":"button2","component":"Button","child":"button2Label","action":{"event":{"name":"readMore","context":{"articleId":"local-sports"}}}},{"id":"button2Label","component":"Text","text":"Read More"}]}} diff --git a/examples/catalog_gallery/samples/notificationCenter.sample b/examples/catalog_gallery/samples/notificationCenter.sample new file mode 100644 index 000000000..7255e5e47 --- /dev/null +++ b/examples/catalog_gallery/samples/notificationCenter.sample @@ -0,0 +1,8 @@ +--- +description: A list of notifications. +name: notificationCenter +prompt: | + Create a notification center. It should have a 'Text' (variant 'h1') "Notifications". A 'List' of 'Card's. Include cards for "New message from Sarah" and "Your order has shipped". Each card should have a 'Button' "Dismiss". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["notificationCenterTitle","notificationList"]},{"id":"notificationCenterTitle","component":"Text","text":"Notifications","variant":"h1"},{"id":"notificationList","component":"List","direction":"vertical","children":["messageNotificationCard","orderShippedNotificationCard"]},{"id":"messageNotificationCard","component":"Card","child":"messageCardContent"},{"id":"messageCardContent","component":"Column","children":["messageText","dismissMessageButton"]},{"id":"messageText","component":"Text","text":"New message from Sarah"},{"id":"dismissMessageButton","component":"Button","child":"dismissMessageButtonText","action":{"event":{"name":"dismissNotification","context":{"notificationId":"messageFromSarah"}}}},{"id":"dismissMessageButtonText","component":"Text","text":"Dismiss"},{"id":"orderShippedNotificationCard","component":"Card","child":"orderShippedCardContent"},{"id":"orderShippedCardContent","component":"Column","children":["orderShippedText","dismissOrderButton"]},{"id":"orderShippedText","component":"Text","text":"Your order has shipped"},{"id":"dismissOrderButton","component":"Button","child":"dismissOrderButtonText","action":{"event":{"name":"dismissNotification","context":{"notificationId":"orderShipped"}}}},{"id":"dismissOrderButtonText","component":"Text","text":"Dismiss"}]}} diff --git a/examples/catalog_gallery/samples/openUrlAction.sample b/examples/catalog_gallery/samples/openUrlAction.sample new file mode 100644 index 000000000..0db6b3148 --- /dev/null +++ b/examples/catalog_gallery/samples/openUrlAction.sample @@ -0,0 +1,10 @@ +--- +description: A button that opens an external URL. +name: openUrlAction +prompt: | + Create a 'createSurface' and 'updateComponents' message. Surface ID 'main'. + Include a 'Button' labeled "Visit Website". + The button's action should be a client-side function call to 'openUrl' with the argument 'url': 'https://a2ui.org'. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["websiteButton"]},{"id":"websiteButton","component":"Button","child":"buttonLabel","action":{"functionCall":{"call":"openUrl","args":{"url":"https://a2ui.org"},"returnType":"void"}}},{"id":"buttonLabel","component":"Text","text":"Visit Website"}]}} diff --git a/examples/catalog_gallery/samples/photoEditor.sample b/examples/catalog_gallery/samples/photoEditor.sample new file mode 100644 index 000000000..770c82137 --- /dev/null +++ b/examples/catalog_gallery/samples/photoEditor.sample @@ -0,0 +1,9 @@ +--- +description: A photo editing interface with sliders. +name: photoEditor +prompt: | + Create a photo editor. It should have a large 'Image' (photo). Below it, a 'Row' of 'Button's (Filters, Crop, Adjust). Below that, a 'Slider' labeled "Intensity" (initialize value to 50). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["photo","buttonRow","intensitySlider"]},{"id":"photo","component":"Image","url":"https://a2ui.org/img/image_placeholder.png","variant":"largeFeature"},{"id":"buttonRow","component":"Row","justify":"spaceBetween","children":["filtersButton","cropButton","adjustButton"]},{"id":"filtersButton","component":"Button","child":"filtersButtonText","action":{"event":{"name":"filters_clicked"}}},{"id":"filtersButtonText","component":"Text","text":"Filters"},{"id":"cropButton","component":"Button","child":"cropButtonText","action":{"event":{"name":"crop_clicked"}}},{"id":"cropButtonText","component":"Text","text":"Crop"},{"id":"adjustButton","component":"Button","child":"adjustButtonText","action":{"event":{"name":"adjust_clicked"}}},{"id":"adjustButtonText","component":"Text","text":"Adjust"},{"id":"intensitySlider","component":"Slider","label":"Intensity","min":0,"max":100,"value":{"path":"/intensity"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"intensity":50}}} diff --git a/examples/catalog_gallery/samples/podcastEpisode.sample b/examples/catalog_gallery/samples/podcastEpisode.sample new file mode 100644 index 000000000..70ecd7f21 --- /dev/null +++ b/examples/catalog_gallery/samples/podcastEpisode.sample @@ -0,0 +1,14 @@ +--- +description: A podcast player interface. +name: podcastEpisode +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a podcast player. 'Card' containing: + - 'Image' (Cover Art). + - 'Text' (h2) "Episode 42: The Future of AI". + - 'Text' "Host: Jane Smith". + - 'Slider' labeled "Progress" (initialize value to 0). + - 'Row' with 'Button' (child 'Text' "1x"), 'Button' (child 'Text' "Play/Pause"), 'Button' (child 'Text' "Share"). + Create these as static components, not data bound. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["podcastCard"]},{"id":"podcastCard","component":"Card","child":"podcastContentColumn"},{"id":"podcastContentColumn","component":"Column","children":["coverArtImage","episodeTitleText","hostText","progressBarSlider","controlsRow"]},{"id":"coverArtImage","component":"Image","url":"https://example.com/cover_art.jpg","variant":"mediumFeature"},{"id":"episodeTitleText","component":"Text","text":"Episode 42: The Future of AI","variant":"h2"},{"id":"hostText","component":"Text","text":"Host: Jane Smith","variant":"body"},{"id":"progressBarSlider","component":"Slider","label":"Progress","min":0,"max":100,"value":0},{"id":"controlsRow","component":"Row","justify":"spaceBetween","children":["speedButton","playPauseButton","shareButton"]},{"id":"speedButton","component":"Button","child":"speedButtonText","action":{"event":{"name":"changeSpeed"}}},{"id":"speedButtonText","component":"Text","text":"1x"},{"id":"playPauseButton","component":"Button","child":"playPauseButtonText","action":{"event":{"name":"togglePlayPause"}}},{"id":"playPauseButtonText","component":"Text","text":"Play/Pause"},{"id":"shareButton","component":"Button","child":"shareButtonText","action":{"event":{"name":"shareEpisode"}}},{"id":"shareButtonText","component":"Text","text":"Share"}]}} diff --git a/examples/catalog_gallery/samples/productGallery.sample b/examples/catalog_gallery/samples/productGallery.sample new file mode 100644 index 000000000..ff6f37213 --- /dev/null +++ b/examples/catalog_gallery/samples/productGallery.sample @@ -0,0 +1,9 @@ +--- +description: A gallery of products using a list with a template. +name: productGallery +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a product gallery. It should display a list of products from the data model at '/products'. Use a template for the list items. Each item should be a Card containing a Column. The Column should contain an Image (from '/products/item/imageUrl'), a Text component for the product name (from '/products/item/name'), and a Button labeled "Add to Cart". The button's action should have a 'event' with 'name': 'addToCart' and a 'context' with the product ID, for example, 'productId': 'static-id-123' (use this exact literal string). You should create a template component and then a list that uses it. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/products","value":{"product1":{"id":"product1","name":"Super Widget","imageUrl":"https://example.com/super_widget.jpg"},"product2":{"id":"product2","name":"Mega Doodad","imageUrl":"https://example.com/mega_doodad.png"},"product3":{"id":"product3","name":"Ultra Gizmo","imageUrl":"https://example.com/ultra_gizmo.jpeg"}}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["productList"]},{"id":"productList","component":"List","direction":"vertical","children":{"componentId":"productCardTemplate","path":"/products"}},{"id":"productCardTemplate","component":"Card","child":"productCardColumn"},{"id":"productCardColumn","component":"Column","children":["productImage","productNameText","addToCartButton"]},{"id":"productImage","component":"Image","url":{"path":"imageUrl"}},{"id":"productNameText","component":"Text","text":{"path":"name"},"variant":"h5"},{"id":"addToCartButton","component":"Button","child":"addToCartButtonText","action":{"event":{"name":"addToCart","context":{"productId":"static-id-123"}}}},{"id":"addToCartButtonText","component":"Text","text":"Add to Cart"}]}} diff --git a/examples/catalog_gallery/samples/profileEditor.sample b/examples/catalog_gallery/samples/profileEditor.sample new file mode 100644 index 000000000..879ef5616 --- /dev/null +++ b/examples/catalog_gallery/samples/profileEditor.sample @@ -0,0 +1,8 @@ +--- +description: A user profile editing form. +name: profileEditor +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for editing a profile. 'Text' (h1) "Edit Profile". 'Image' (Current Avatar). 'Button' "Change Photo". 'TextField' "Display Name". 'TextField' "Bio" (multiline). 'TextField' "Website". 'Button' "Save Changes". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["editProfileTitle","currentAvatarImage","changePhotoButton","displayNameTextField","bioTextField","websiteTextField","saveChangesButton"]},{"id":"editProfileTitle","component":"Text","text":"Edit Profile","variant":"h1"},{"id":"currentAvatarImage","component":"Image","url":"https://a2ui.org/img/avatar_placeholder.png","variant":"avatar"},{"id":"changePhotoButton","component":"Button","child":"changePhotoText","action":{"event":{"name":"changePhoto"}}},{"id":"changePhotoText","component":"Text","text":"Change Photo"},{"id":"displayNameTextField","component":"TextField","label":"Display Name","value":{"path":"/profile/displayName"}},{"id":"bioTextField","component":"TextField","label":"Bio","value":{"path":"/profile/bio"},"variant":"longText"},{"id":"websiteTextField","component":"TextField","label":"Website","value":{"path":"/profile/website"},"checks":[{"condition":{"call":"regex","args":{"value":{"path":"/profile/website"},"pattern":"^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$"},"returnType":"boolean"},"message":"Please enter a valid URL."}]},{"id":"saveChangesButton","component":"Button","child":"saveChangesText","variant":"primary","action":{"event":{"name":"saveProfileChanges"}}},{"id":"saveChangesText","component":"Text","text":"Save Changes"}]}} diff --git a/examples/catalog_gallery/samples/recipeCard.sample b/examples/catalog_gallery/samples/recipeCard.sample new file mode 100644 index 000000000..2fe186388 --- /dev/null +++ b/examples/catalog_gallery/samples/recipeCard.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display a recipe with ingredients and instructions. +name: recipeCard +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a recipe card. It should have a 'Text' (variant 'h1') for the recipe title, "Classic Lasagna". Below the title, an 'Image' of the lasagna. Then, a 'Row' containing two 'Column's. The first column has a 'Text' (variant 'h2') "Ingredients" and a 'List' of ingredients (use 'Text' components for items: "Pasta", "Cheese", "Sauce"). The second column has a 'Text' (variant 'h2') "Instructions" and a 'List' of step-by-step instructions (use 'Text' components: "Boil pasta", "Layer ingredients", "Bake"). Finally, a 'Button' at the bottom labeled "Watch Video Tutorial". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["recipeTitle","recipeImage","contentRow","watchVideoButton"]},{"id":"recipeTitle","component":"Text","text":"Classic Lasagna","variant":"h1"},{"id":"recipeImage","component":"Image","url":"https://example.com/lasagna.jpg","fit":"cover"},{"id":"contentRow","component":"Row","children":["ingredientsColumn","instructionsColumn"],"justify":"spaceBetween"},{"id":"ingredientsColumn","component":"Column","weight":1,"children":["ingredientsTitle","ingredientsList"]},{"id":"ingredientsTitle","component":"Text","text":"Ingredients","variant":"h2"},{"id":"ingredientsList","component":"List","children":["ingredient1","ingredient2","ingredient3"],"direction":"vertical"},{"id":"ingredient1","component":"Text","text":"Pasta"},{"id":"ingredient2","component":"Text","text":"Cheese"},{"id":"ingredient3","component":"Text","text":"Sauce"},{"id":"instructionsColumn","component":"Column","weight":1,"children":["instructionsTitle","instructionsList"]},{"id":"instructionsTitle","component":"Text","text":"Instructions","variant":"h2"},{"id":"instructionsList","component":"List","children":["instruction1","instruction2","instruction3"],"direction":"vertical"},{"id":"instruction1","component":"Text","text":"Boil pasta"},{"id":"instruction2","component":"Text","text":"Layer ingredients"},{"id":"instruction3","component":"Text","text":"Bake"},{"id":"watchVideoButton","component":"Button","child":"watchVideoText","action":{"functionCall":{"call":"openUrl","args":{"url":"https://example.com/lasagna-tutorial"},"returnType":"void"}}},{"id":"watchVideoText","component":"Text","text":"Watch Video Tutorial"}]}} diff --git a/examples/catalog_gallery/samples/restaurantMenu.sample b/examples/catalog_gallery/samples/restaurantMenu.sample new file mode 100644 index 000000000..ff6a8d086 --- /dev/null +++ b/examples/catalog_gallery/samples/restaurantMenu.sample @@ -0,0 +1,11 @@ +--- +description: A restaurant menu with tabs. +name: restaurantMenu +prompt: | + Create a restaurant menu with tabs. It should have a 'Text' (variant 'h1') "Gourmet Bistro". A 'Tabs' component with "Starters", "Mains", "Desserts". + - "Starters": 'List' containing IDs of separate 'Row' components (Name, Price). Create rows for "Soup - $8", "Salad - $10". + - "Mains": 'List' containing IDs of separate 'Row' components. Create rows for "Steak - $25", "Pasta - $18". + - "Desserts": 'List' containing IDs of separate 'Row' components. Create rows for "Cake - $8", "Pie - $7". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["restaurantTitle","menuTabs"]},{"id":"restaurantTitle","component":"Text","text":"Gourmet Bistro","variant":"h1"},{"id":"menuTabs","component":"Tabs","tabs":[{"title":"Starters","child":"startersList"},{"title":"Mains","child":"mainsList"},{"title":"Desserts","child":"dessertsList"}]},{"id":"startersList","component":"List","direction":"vertical","children":["soupRow","saladRow"]},{"id":"soupRow","component":"Row","children":["soupText","soupPrice"],"justify":"spaceBetween"},{"id":"soupText","component":"Text","text":"Soup"},{"id":"soupPrice","component":"Text","text":"$8"},{"id":"saladRow","component":"Row","children":["saladText","saladPrice"],"justify":"spaceBetween"},{"id":"saladText","component":"Text","text":"Salad"},{"id":"saladPrice","component":"Text","text":"$10"},{"id":"mainsList","component":"List","direction":"vertical","children":["steakRow","pastaRow"]},{"id":"steakRow","component":"Row","children":["steakText","steakPrice"],"justify":"spaceBetween"},{"id":"steakText","component":"Text","text":"Steak"},{"id":"steakPrice","component":"Text","text":"$25"},{"id":"pastaRow","component":"Row","children":["pastaText","pastaPrice"],"justify":"spaceBetween"},{"id":"pastaText","component":"Text","text":"Pasta"},{"id":"pastaPrice","component":"Text","text":"$18"},{"id":"dessertsList","component":"List","direction":"vertical","children":["cakeRow","pieRow"]},{"id":"cakeRow","component":"Row","children":["cakeText","cakePrice"],"justify":"spaceBetween"},{"id":"cakeText","component":"Text","text":"Cake"},{"id":"cakePrice","component":"Text","text":"$8"},{"id":"pieRow","component":"Row","children":["pieText","piePrice"],"justify":"spaceBetween"},{"id":"pieText","component":"Text","text":"Pie"},{"id":"piePrice","component":"Text","text":"$7"}]}} diff --git a/examples/catalog_gallery/samples/settingsPage.sample b/examples/catalog_gallery/samples/settingsPage.sample new file mode 100644 index 000000000..439a46fe9 --- /dev/null +++ b/examples/catalog_gallery/samples/settingsPage.sample @@ -0,0 +1,9 @@ +--- +description: A settings page with tabs and a modal dialog. +name: settingsPage +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a user settings page. Use a Tabs component with two tabs: "Profile" and "Notifications". The "Profile" tab should contain a simple column with a text field for the user's name. The "Notifications" tab should contain a checkbox for "Enable email notifications". Also, include a Modal component. The modal's trigger should be a button labeled "Delete Account", and its content should be a column with a confirmation text and two buttons: "Confirm Deletion" and "Cancel". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["settingsTabs","deleteAccountModal"]},{"id":"settingsTabs","component":"Tabs","tabs":[{"title":"Profile","child":"profileTabContent"},{"title":"Notifications","child":"notificationsTabContent"}]},{"id":"profileTabContent","component":"Column","children":["userNameTextField"]},{"id":"userNameTextField","component":"TextField","label":"Name","value":{"path":"/user/name"}},{"id":"notificationsTabContent","component":"Column","children":["emailNotificationsCheckbox"]},{"id":"emailNotificationsCheckbox","component":"CheckBox","label":"Enable email notifications","value":{"path":"/user/emailNotificationsEnabled"}},{"id":"deleteAccountModal","component":"Modal","trigger":"deleteAccountButton","content":"deleteConfirmationContent"},{"id":"deleteAccountButton","component":"Button","child":"deleteAccountButtonText","action":{"event":{"name":"openDeleteAccountModal"}}},{"id":"deleteAccountButtonText","component":"Text","text":"Delete Account"},{"id":"deleteConfirmationContent","component":"Column","children":["confirmationText","modalButtonsRow"]},{"id":"confirmationText","component":"Text","text":"Are you sure you want to delete your account? This action cannot be undone."},{"id":"modalButtonsRow","component":"Row","justify":"spaceBetween","children":["confirmDeletionButton","cancelDeletionButton"]},{"id":"confirmDeletionButton","component":"Button","child":"confirmDeletionButtonText","action":{"event":{"name":"confirmAccountDeletion"}}},{"id":"confirmDeletionButtonText","component":"Text","text":"Confirm Deletion"},{"id":"cancelDeletionButton","component":"Button","child":"cancelDeletionButtonText","action":{"event":{"name":"cancelAccountDeletion"}}},{"id":"cancelDeletionButtonText","component":"Text","text":"Cancel"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"user":{"name":"John Doe","emailNotificationsEnabled":true}}}} diff --git a/examples/catalog_gallery/samples/simpleCalculator.sample b/examples/catalog_gallery/samples/simpleCalculator.sample new file mode 100644 index 000000000..cc9f3c6ae --- /dev/null +++ b/examples/catalog_gallery/samples/simpleCalculator.sample @@ -0,0 +1,13 @@ +--- +description: A basic calculator layout. +name: simpleCalculator +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a calculator. It should have a 'Card'. Inside the card, there MUST be a single 'Column' that contains two things: a 'Text' (display) showing "0", and a nested 'Column' of 'Row's for the buttons. + - Row 1: "7", "8", "9", "/" + - Row 2: "4", "5", "6", "*" + - Row 3: "1", "2", "3", "-" + - Row 4: "0", ".", "=", "+" + Each button should be a 'Button' component with a child 'Text' component for the label (e.g. '7', '+'). +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"calculatorColumn"},{"id":"calculatorColumn","component":"Column","children":["display","buttonsColumn"]},{"id":"display","component":"Text","text":"0","variant":"h3"},{"id":"buttonsColumn","component":"Column","children":["row1","row2","row3","row4"]},{"id":"row1","component":"Row","children":["button7","button8","button9","buttonDivide"],"justify":"spaceBetween"},{"id":"button7","component":"Button","child":"text7","action":{"event":{"name":"digitPress","context":{"digit":"7"}}}},{"id":"text7","component":"Text","text":"7"},{"id":"button8","component":"Button","child":"text8","action":{"event":{"name":"digitPress","context":{"digit":"8"}}}},{"id":"text8","component":"Text","text":"8"},{"id":"button9","component":"Button","child":"text9","action":{"event":{"name":"digitPress","context":{"digit":"9"}}}},{"id":"text9","component":"Text","text":"9"},{"id":"buttonDivide","component":"Button","child":"textDivide","action":{"event":{"name":"operatorPress","context":{"operator":"/"}}}},{"id":"textDivide","component":"Text","text":"/"},{"id":"row2","component":"Row","children":["button4","button5","button6","buttonMultiply"],"justify":"spaceBetween"},{"id":"button4","component":"Button","child":"text4","action":{"event":{"name":"digitPress","context":{"digit":"4"}}}},{"id":"text4","component":"Text","text":"4"},{"id":"button5","component":"Button","child":"text5","action":{"event":{"name":"digitPress","context":{"digit":"5"}}}},{"id":"text5","component":"Text","text":"5"},{"id":"button6","component":"Button","child":"text6","action":{"event":{"name":"digitPress","context":{"digit":"6"}}}},{"id":"text6","component":"Text","text":"6"},{"id":"buttonMultiply","component":"Button","child":"textMultiply","action":{"event":{"name":"operatorPress","context":{"operator":"*"}}}},{"id":"textMultiply","component":"Text","text":"*"},{"id":"row3","component":"Row","children":["button1","button2","button3","buttonMinus"],"justify":"spaceBetween"},{"id":"button1","component":"Button","child":"text1","action":{"event":{"name":"digitPress","context":{"digit":"1"}}}},{"id":"text1","component":"Text","text":"1"},{"id":"button2","component":"Button","child":"text2","action":{"event":{"name":"digitPress","context":{"digit":"2"}}}},{"id":"text2","component":"Text","text":"2"},{"id":"button3","component":"Button","child":"text3","action":{"event":{"name":"digitPress","context":{"digit":"3"}}}},{"id":"text3","component":"Text","text":"3"},{"id":"buttonMinus","component":"Button","child":"textMinus","action":{"event":{"name":"operatorPress","context":{"operator":"-"}}}},{"id":"textMinus","component":"Text","text":"-"},{"id":"row4","component":"Row","children":["button0","buttonDot","buttonEquals","buttonPlus"],"justify":"spaceBetween"},{"id":"button0","component":"Button","child":"text0","action":{"event":{"name":"digitPress","context":{"digit":"0"}}}},{"id":"text0","component":"Text","text":"0"},{"id":"buttonDot","component":"Button","child":"textDot","action":{"event":{"name":"decimalPress"}}},{"id":"textDot","component":"Text","text":"."},{"id":"buttonEquals","component":"Button","child":"textEquals","action":{"event":{"name":"calculate"}}},{"id":"textEquals","component":"Text","text":"="},{"id":"buttonPlus","component":"Button","child":"textPlus","action":{"event":{"name":"operatorPress","context":{"operator":"+"}}}},{"id":"textPlus","component":"Text","text":"+"}]}} diff --git a/examples/catalog_gallery/samples/smartHome.sample b/examples/catalog_gallery/samples/smartHome.sample new file mode 100644 index 000000000..e8e5a4dcd --- /dev/null +++ b/examples/catalog_gallery/samples/smartHome.sample @@ -0,0 +1,8 @@ +--- +description: A smart home control panel. +name: smartHome +prompt: | + Create a smart home dashboard. It should have a 'Text' (variant 'h1') "Living Room". A 'Grid' of 'Card's. To create the grid, use a 'Column' that contains multiple 'Row's. Each 'Row' should contain 'Card's. Create a row with cards for "Lights" (CheckBox, label "Lights", value true) and "Thermostat" (Slider, label "Thermostat", value 72). Create another row with a card for "Music" (CheckBox, label "Music", value false). Ensure the CheckBox labels are exactly "Lights" and "Music". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["livingRoomTitle","gridColumn"]},{"id":"livingRoomTitle","component":"Text","text":"Living Room","variant":"h1"},{"id":"gridColumn","component":"Column","children":["firstRow","secondRow"]},{"id":"firstRow","component":"Row","children":["lightsCard","thermostatCard"],"justify":"spaceEvenly"},{"id":"lightsCard","component":"Card","child":"lightsCheckbox","weight":1},{"id":"lightsCheckbox","component":"CheckBox","label":"Lights","value":true},{"id":"thermostatCard","component":"Card","child":"thermostatSlider","weight":1},{"id":"thermostatSlider","component":"Slider","label":"Thermostat","min":60,"max":80,"value":72},{"id":"secondRow","component":"Row","children":["musicCard"],"justify":"spaceEvenly"},{"id":"musicCard","component":"Card","child":"musicCheckbox","weight":1},{"id":"musicCheckbox","component":"CheckBox","label":"Music","value":false}]}} diff --git a/examples/catalog_gallery/samples/socialMediaPost.sample b/examples/catalog_gallery/samples/socialMediaPost.sample new file mode 100644 index 000000000..5d1453f30 --- /dev/null +++ b/examples/catalog_gallery/samples/socialMediaPost.sample @@ -0,0 +1,8 @@ +--- +description: A component representing a social media post. +name: socialMediaPost +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a social media post. It should be a 'Card' containing a 'Column'. The first item is a 'Row' with an 'Image' (user avatar) and a 'Text' (username "user123"). Below that, a 'Text' component for the post content: "Enjoying the beautiful weather today!". Then, an 'Image' for the main post picture. Finally, a 'Row' with three 'Button's: "Like", "Comment", and "Share". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Card","child":"postColumn"},{"id":"postColumn","component":"Column","children":["userInfoRow","postContentText","postImage","actionsRow"]},{"id":"userInfoRow","component":"Row","children":["userAvatar","usernameText"],"align":"center"},{"id":"userAvatar","component":"Image","url":"https://a2ui.org/img/avatar.png","variant":"avatar"},{"id":"usernameText","component":"Text","text":"user123","variant":"h5"},{"id":"postContentText","component":"Text","text":"Enjoying the beautiful weather today!","variant":"body"},{"id":"postImage","component":"Image","url":"https://a2ui.org/img/placeholder.png","variant":"mediumFeature","fit":"cover"},{"id":"actionsRow","component":"Row","children":["likeButton","commentButton","shareButton"],"justify":"spaceBetween"},{"id":"likeButton","component":"Button","child":"likeButtonText","action":{"event":{"name":"likePost"}},"variant":"borderless"},{"id":"likeButtonText","component":"Text","text":"Like"},{"id":"commentButton","component":"Button","child":"commentButtonText","action":{"event":{"name":"commentPost"}},"variant":"borderless"},{"id":"commentButtonText","component":"Text","text":"Comment"},{"id":"shareButton","component":"Button","child":"shareButtonText","action":{"event":{"name":"sharePost"}},"variant":"borderless"},{"id":"shareButtonText","component":"Text","text":"Share"}]}} diff --git a/examples/catalog_gallery/samples/standardFunctions.sample b/examples/catalog_gallery/samples/standardFunctions.sample new file mode 100644 index 000000000..937f2abbc --- /dev/null +++ b/examples/catalog_gallery/samples/standardFunctions.sample @@ -0,0 +1,15 @@ +--- +description: Usage of pluralize. +name: standardFunctions +prompt: | + Create a 'createSurface' and 'updateComponents' message for a shopping cart summary. Surface ID 'main'. + Display a 'Text' component. + The text value should be a 'pluralize' function call with returnType 'string'. + The pluralize call should use the count from '/cart/count' and provide these options: + 'zero': "No items" + 'one': "One item" + 'other': "${/cart/count} items" +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["cartSummaryText"]},{"id":"cartSummaryText","component":"Text","text":{"call":"pluralize","args":{"count":{"path":"/cart/count"},"zero":"No items","one":"One item","other":"${/cart/count} items"},"returnType":"string"}}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/cart/count","value":2}} diff --git a/examples/catalog_gallery/samples/stockWatchlist.sample b/examples/catalog_gallery/samples/stockWatchlist.sample new file mode 100644 index 000000000..fe242fa51 --- /dev/null +++ b/examples/catalog_gallery/samples/stockWatchlist.sample @@ -0,0 +1,11 @@ +--- +description: A stock market watchlist. +name: stockWatchlist +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a stock watchlist. 'Text' (h1) "Market Watch". 'List' of 'Row's. + - Row 1: 'Text' "AAPL", 'Text' "$150.00", 'Text' "+1.2%". + - Row 2: 'Text' "GOOGL", 'Text' "$2800.00", 'Text' "-0.5%". + - Row 3: 'Text' "AMZN", 'Text' "$3400.00", 'Text' "+0.8%". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["marketWatchTitle","stockList"]},{"id":"marketWatchTitle","component":"Text","text":"Market Watch","variant":"h1"},{"id":"stockList","component":"List","children":["aaplRow","googlRow","amznRow"]},{"id":"aaplRow","component":"Row","children":["aaplSymbol","aaplPrice","aaplChange"],"justify":"spaceBetween"},{"id":"aaplSymbol","component":"Text","text":"AAPL"},{"id":"aaplPrice","component":"Text","text":"$150.00"},{"id":"aaplChange","component":"Text","text":"+1.2%"},{"id":"googlRow","component":"Row","children":["googlSymbol","googlPrice","googlChange"],"justify":"spaceBetween"},{"id":"googlSymbol","component":"Text","text":"GOOGL"},{"id":"googlPrice","component":"Text","text":"$2800.00"},{"id":"googlChange","component":"Text","text":"-0.5%"},{"id":"amznRow","component":"Row","children":["amznSymbol","amznPrice","amznChange"],"justify":"spaceBetween"},{"id":"amznSymbol","component":"Text","text":"AMZN"},{"id":"amznPrice","component":"Text","text":"$3400.00"},{"id":"amznChange","component":"Text","text":"+0.8%"}]}} diff --git a/examples/catalog_gallery/samples/surveyForm.sample b/examples/catalog_gallery/samples/surveyForm.sample new file mode 100644 index 000000000..5188c1253 --- /dev/null +++ b/examples/catalog_gallery/samples/surveyForm.sample @@ -0,0 +1,9 @@ +--- +description: A customer feedback survey form. +name: surveyForm +prompt: | + Create a customer feedback survey form. It should have a 'Text' (variant 'h1') "Customer Feedback". Then a 'ChoicePicker' (variant 'mutuallyExclusive') with label "How would you rate our service?" and options "Excellent", "Good", "Average", "Poor". Then a 'ChoicePicker' (variant 'multipleSelection') with label "What did you like?" and options "Product Quality", "Price", "Customer Support". Finally, a 'TextField' with the label "Any other comments?" and a 'Button' labeled "Submit Feedback". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","value":{"serviceRating":"","likedItems":[],"comments":""}}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["customerFeedbackTitle","serviceRatingPicker","likedItemsPicker","commentsTextField","submitButton"]},{"id":"customerFeedbackTitle","component":"Text","text":"Customer Feedback","variant":"h1"},{"id":"serviceRatingPicker","component":"ChoicePicker","label":"How would you rate our service?","variant":"mutuallyExclusive","options":[{"label":"Excellent","value":"excellent"},{"label":"Good","value":"good"},{"label":"Average","value":"average"},{"label":"Poor","value":"poor"}],"value":{"path":"/serviceRating"}},{"id":"likedItemsPicker","component":"ChoicePicker","label":"What did you like?","variant":"multipleSelection","options":[{"label":"Product Quality","value":"product_quality"},{"label":"Price","value":"price"},{"label":"Customer Support","value":"customer_support"}],"value":{"path":"/likedItems"}},{"id":"commentsTextField","component":"TextField","label":"Any other comments?","variant":"longText","value":{"path":"/comments"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitFeedback","context":{"serviceRating":{"path":"/serviceRating"},"likedItems":{"path":"/likedItems"},"comments":{"path":"/comments"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Feedback"}]}} diff --git a/examples/catalog_gallery/samples/travelItinerary.sample b/examples/catalog_gallery/samples/travelItinerary.sample new file mode 100644 index 000000000..bae388f0f --- /dev/null +++ b/examples/catalog_gallery/samples/travelItinerary.sample @@ -0,0 +1,14 @@ +--- +description: A multi-day travel itinerary display. +name: travelItinerary +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a travel itinerary for a trip to Paris. + It should have a main 'Text' component with variant 'h1' and text "Paris Adventure". + Below, use a 'List' to display three days. Each item in the list should be a 'Card'. + - The first 'Card' (Day 1) should contain a 'Text' (variant 'h2') "Day 1: Arrival & Eiffel Tower", and a 'List' of activities for that day: "Check into hotel", "Lunch at a cafe", "Visit the Eiffel Tower". + - The second 'Card' (Day 2) should contain a 'Text' (variant 'h2') "Day 2: Museums & Culture", and a 'List' of activities: "Visit the Louvre Museum", "Walk through Tuileries Garden", "See the Arc de Triomphe". + - The third 'Card' (Day 3) should contain a 'Text' (variant 'h2') "Day 3: Art & Departure", and a 'List' of activities: "Visit Musée d'Orsay", "Explore Montmartre", "Depart from CDG". + Each activity in the inner lists should be a 'Row' containing a 'CheckBox' (to mark as complete, with an empty label '') and a 'Text' component with the activity description. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["parisAdventureTitle","itineraryList"]},{"id":"parisAdventureTitle","component":"Text","text":"Paris Adventure","variant":"h1"},{"id":"itineraryList","component":"List","children":["day1Card","day2Card","day3Card"],"direction":"vertical"},{"id":"day1Card","component":"Card","child":"day1Content"},{"id":"day1Content","component":"Column","children":["day1Title","day1Activities"]},{"id":"day1Title","component":"Text","text":"Day 1: Arrival & Eiffel Tower","variant":"h2"},{"id":"day1Activities","component":"List","children":["day1Activity1","day1Activity2","day1Activity3"],"direction":"vertical"},{"id":"day1Activity1","component":"Row","children":["day1Checkbox1","day1Text1"]},{"id":"day1Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day1Text1","component":"Text","text":"Check into hotel"},{"id":"day1Activity2","component":"Row","children":["day1Checkbox2","day1Text2"]},{"id":"day1Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day1Text2","component":"Text","text":"Lunch at a cafe"},{"id":"day1Activity3","component":"Row","children":["day1Checkbox3","day1Text3"]},{"id":"day1Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day1Text3","component":"Text","text":"Visit the Eiffel Tower"},{"id":"day2Card","component":"Card","child":"day2Content"},{"id":"day2Content","component":"Column","children":["day2Title","day2Activities"]},{"id":"day2Title","component":"Text","text":"Day 2: Museums & Culture","variant":"h2"},{"id":"day2Activities","component":"List","children":["day2Activity1","day2Activity2","day2Activity3"],"direction":"vertical"},{"id":"day2Activity1","component":"Row","children":["day2Checkbox1","day2Text1"]},{"id":"day2Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day2Text1","component":"Text","text":"Visit the Louvre Museum"},{"id":"day2Activity2","component":"Row","children":["day2Checkbox2","day2Text2"]},{"id":"day2Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day2Text2","component":"Text","text":"Walk through Tuileries Garden"},{"id":"day2Activity3","component":"Row","children":["day2Checkbox3","day2Text3"]},{"id":"day2Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day2Text3","component":"Text","text":"See the Arc de Triomphe"},{"id":"day3Card","component":"Card","child":"day3Content"},{"id":"day3Content","component":"Column","children":["day3Title","day3Activities"]},{"id":"day3Title","component":"Text","text":"Day 3: Art & Departure","variant":"h2"},{"id":"day3Activities","component":"List","children":["day3Activity1","day3Activity2","day3Activity3"],"direction":"vertical"},{"id":"day3Activity1","component":"Row","children":["day3Checkbox1","day3Text1"]},{"id":"day3Checkbox1","component":"CheckBox","label":"","value":false},{"id":"day3Text1","component":"Text","text":"Visit Musée d'Orsay"},{"id":"day3Activity2","component":"Row","children":["day3Checkbox2","day3Text2"]},{"id":"day3Checkbox2","component":"CheckBox","label":"","value":false},{"id":"day3Text2","component":"Text","text":"Explore Montmartre"},{"id":"day3Activity3","component":"Row","children":["day3Checkbox3","day3Text3"]},{"id":"day3Checkbox3","component":"CheckBox","label":"","value":false},{"id":"day3Text3","component":"Text","text":"Depart from CDG"}]}} diff --git a/examples/catalog_gallery/samples/triviaQuiz.sample b/examples/catalog_gallery/samples/triviaQuiz.sample new file mode 100644 index 000000000..459c124fc --- /dev/null +++ b/examples/catalog_gallery/samples/triviaQuiz.sample @@ -0,0 +1,9 @@ +--- +description: A trivia question card. +name: triviaQuiz +prompt: | + Create a trivia quiz. It should have a 'Text' (variant 'h1') "Question 1". A 'Text' "What is the capital of France?". A 'ChoicePicker' (variant 'mutuallyExclusive') for answers (options: "Paris", "London", "Berlin", "Madrid"). A 'Button' "Submit Answer". +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["question1Title","question1Text","answerChoicePicker","submitButton"]},{"id":"question1Title","component":"Text","variant":"h1","text":"Question 1"},{"id":"question1Text","component":"Text","text":"What is the capital of France?"},{"id":"answerChoicePicker","component":"ChoicePicker","label":"Select your answer","variant":"mutuallyExclusive","options":[{"label":"Paris","value":"Paris"},{"label":"London","value":"London"},{"label":"Berlin","value":"Berlin"},{"label":"Madrid","value":"Madrid"}],"value":{"path":"/selectedAnswer"}},{"id":"submitButton","component":"Button","child":"submitButtonText","action":{"event":{"name":"submitAnswer","context":{"answer":{"path":"/selectedAnswer"}}}}},{"id":"submitButtonText","component":"Text","text":"Submit Answer"}]}} +{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/","value":{"selectedAnswer":[]}}} diff --git a/examples/catalog_gallery/samples/videoCallInterface.sample b/examples/catalog_gallery/samples/videoCallInterface.sample new file mode 100644 index 000000000..735e6089b --- /dev/null +++ b/examples/catalog_gallery/samples/videoCallInterface.sample @@ -0,0 +1,8 @@ +--- +description: A video conference UI. +name: videoCallInterface +prompt: | + Create a video call interface. It should have a 'Text' (variant 'h1') "Video Call". A 'Video' component with a valid placeholder URL (e.g. 'https://example.com/video.mp4'). Below that, a 'Row' with three 'Button's, each with a child 'Text' component with the text "Mute", "Camera", and "End Call" respectively. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["videoCallTitle","videoDisplay","callControls"],"justify":"spaceBetween","align":"center"},{"id":"videoCallTitle","component":"Text","text":"Video Call","variant":"h1"},{"id":"videoDisplay","component":"Video","url":"https://example.com/video.mp4"},{"id":"callControls","component":"Row","children":["muteButton","cameraButton","endCallButton"],"justify":"spaceEvenly","align":"center"},{"id":"muteButton","component":"Button","child":"muteButtonText","action":{"event":{"name":"muteToggle"}}},{"id":"muteButtonText","component":"Text","text":"Mute"},{"id":"cameraButton","component":"Button","child":"cameraButtonText","action":{"event":{"name":"cameraToggle"}}},{"id":"cameraButtonText","component":"Text","text":"Camera"},{"id":"endCallButton","component":"Button","child":"endCallButtonText","action":{"event":{"name":"endCall"}}},{"id":"endCallButtonText","component":"Text","text":"End Call"}]}} diff --git a/examples/catalog_gallery/samples/weatherForecast.sample b/examples/catalog_gallery/samples/weatherForecast.sample new file mode 100644 index 000000000..cbde8f6fc --- /dev/null +++ b/examples/catalog_gallery/samples/weatherForecast.sample @@ -0,0 +1,8 @@ +--- +description: A UI to display the weather forecast. +name: weatherForecast +prompt: | + Generate a 'createSurface' message and a 'updateComponents' message with surfaceId 'main' for a weather forecast UI. It should have a 'Text' (variant 'h1') with the city name, "New York". Below it, a 'Row' with the current temperature as a 'Text' component ("68°F") and an 'Image' for the weather icon (e.g., a sun). Below that, a 'Divider'. Then, a 'List' component to display the 5-day forecast. Each item in the list should be a 'Row' with the day, an icon, and high/low temperatures. +--- +{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/standard_catalog.json"}} +{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[{"id":"root","component":"Column","children":["cityName","currentWeatherRow","forecastDivider","forecastList"]},{"id":"cityName","component":"Text","variant":"h1","text":"New York"},{"id":"currentWeatherRow","component":"Row","align":"center","children":["currentTemperature","weatherIcon"]},{"id":"currentTemperature","component":"Text","text":"68°F"},{"id":"weatherIcon","component":"Image","url":"https://a2ui.org/img/sun.png","variant":"icon"},{"id":"forecastDivider","component":"Divider","axis":"horizontal"},{"id":"forecastList","component":"List","direction":"vertical","children":["day1Forecast","day2Forecast","day3Forecast","day4Forecast","day5Forecast"]},{"id":"day1Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day1Text","day1Icon","day1Temp"]},{"id":"day1Text","component":"Text","text":"Mon"},{"id":"day1Icon","component":"Icon","name":"cloud"},{"id":"day1Temp","component":"Text","text":"H:72° L:58°"},{"id":"day2Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day2Text","day2Icon","day2Temp"]},{"id":"day2Text","component":"Text","text":"Tue"},{"id":"day2Icon","component":"Icon","name":"wbSunny"},{"id":"day2Temp","component":"Text","text":"H:75° L:60°"},{"id":"day3Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day3Text","day3Icon","day3Temp"]},{"id":"day3Text","component":"Text","text":"Wed"},{"id":"day3Icon","component":"Icon","name":"rainy"},{"id":"day3Temp","component":"Text","text":"H:65° L:55°"},{"id":"day4Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day4Text","day4Icon","day4Temp"]},{"id":"day4Text","component":"Text","text":"Thu"},{"id":"day4Icon","component":"Icon","name":"cloud"},{"id":"day4Temp","component":"Text","text":"H:68° L:57°"},{"id":"day5Forecast","component":"Row","align":"center","justify":"spaceBetween","children":["day5Text","day5Icon","day5Temp"]},{"id":"day5Text","component":"Text","text":"Fri"},{"id":"day5Icon","component":"Icon","name":"wbSunny"},{"id":"day5Temp","component":"Text","text":"H:70° L:62°"}]}} diff --git a/examples/catalog_gallery/test/layout_test.dart b/examples/catalog_gallery/test/layout_test.dart new file mode 100644 index 000000000..286aaa099 --- /dev/null +++ b/examples/catalog_gallery/test/layout_test.dart @@ -0,0 +1,93 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:catalog_gallery/sample_parser.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +import 'src/test_http_client.dart'; + +void main() { + testWidgets('cinemaSeatSelection sample renders without error', ( + WidgetTester tester, + ) async { + HttpOverrides.global = TestHttpOverrides(); + addTearDown(() => HttpOverrides.global = null); + addTearDown(() => debugNetworkImageHttpClientProvider = null); + + final file = File('samples/cinemaSeatSelection.sample'); + final String content = file.readAsStringSync(); + final Sample sample = SampleParser.parseString(content); + + final controller = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], + ); + + await for (final A2uiMessage message in sample.messages) { + var messageToProcess = message; + if (message is CreateSurface) { + // We manually inject the basic catalog since createSurface might ref + // external URL in this test environment, we just assume the basic + // catalog is available + messageToProcess = CreateSurface( + surfaceId: message.surfaceId, + catalogId: basicCatalogId, + theme: message.theme, + sendDataModel: message.sendDataModel, + ); + } + controller.handleMessage(messageToProcess); + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('main')), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Select Seats'), findsOneWidget); + }); + + testWidgets('nestedLayoutRecursive sample renders without error', ( + WidgetTester tester, + ) async { + debugNetworkImageHttpClientProvider = TestHttpClient.new; + // addTearDown(() => debugNetworkImageHttpClientProvider = null); + + try { + final file = File('samples/nestedLayoutRecursive.sample'); + final String content = file.readAsStringSync(); + final Sample sample = SampleParser.parseString(content); + + final controller = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], + ); + + await for (final A2uiMessage message in sample.messages) { + controller.handleMessage(message); + } + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('main')), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Deep content'), findsOneWidget); + } finally { + debugNetworkImageHttpClientProvider = null; + } + }); +} diff --git a/examples/catalog_gallery/test/sample_parser_test.dart b/examples/catalog_gallery/test/sample_parser_test.dart index da3ea0e9a..553adf94d 100644 --- a/examples/catalog_gallery/test/sample_parser_test.dart +++ b/examples/catalog_gallery/test/sample_parser_test.dart @@ -12,8 +12,8 @@ void main() { name: Test Sample description: A test description --- -{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello"}}}}]}} -{"beginRendering": {"surfaceId": "default", "root": "text1"}} +{"version": "v0.9", "updateComponents": {"surfaceId": "default", "components": [{"id": "text1", "component": "Text", "text": "Hello"}]}} +{"version": "v0.9", "createSurface": {"surfaceId": "default", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json"}} '''; final Sample sample = SampleParser.parseString(sampleContent); @@ -23,17 +23,46 @@ description: A test description final List messages = await sample.messages.toList(); expect(messages.length, 2); - expect(messages.first, isA()); - expect(messages.last, isA()); + expect(messages.first, isA()); + expect(messages.last, isA()); - final update = messages.first as SurfaceUpdate; + final update = messages.first as UpdateComponents; expect(update.surfaceId, 'default'); expect(update.components.length, 1); expect(update.components.first.type, 'Text'); - final begin = messages.last as BeginRendering; + final begin = messages.last as CreateSurface; expect(begin.surfaceId, 'default'); - expect(begin.root, 'text1'); + // begin.root check removed as it doesn't exist in CreateSurface + }); + + test( + 'SampleParser parses sample with frontmatter (leading dashes)', + () async { + const sampleContent = ''' +--- +name: Frontmatter Sample +description: A description +--- +{"version": "v0.9", "createSurface": {"surfaceId": "default", "catalogId": "test"}} +'''; + final Sample sample = SampleParser.parseString(sampleContent); + expect(sample.name, 'Frontmatter Sample'); + final List messages = await sample.messages.toList(); + expect(messages.length, 1); + }, + ); + + test('SampleParser parses sample with empty header', () async { + const sampleContent = ''' +--- +--- +{"version": "v0.9", "createSurface": {"surfaceId": "default", "catalogId": "test"}} +'''; + final Sample sample = SampleParser.parseString(sampleContent); + expect(sample.name, 'Untitled Sample'); + final List messages = await sample.messages.toList(); + expect(messages.length, 1); }); test('SampleParser throws on missing separator', () { diff --git a/examples/catalog_gallery/test/samples_rendering_test.dart b/examples/catalog_gallery/test/samples_rendering_test.dart new file mode 100644 index 000000000..8f3b96bd5 --- /dev/null +++ b/examples/catalog_gallery/test/samples_rendering_test.dart @@ -0,0 +1,177 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:catalog_gallery/sample_parser.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +import 'src/test_http_client.dart'; + +void main() { + const fs = LocalFileSystem(); + Directory? samplesDir; + + // Locate samples directory synchronously before tests run + final Directory current = fs.currentDirectory; + if (current.childDirectory('samples').existsSync()) { + samplesDir = current.childDirectory('samples'); + } else if (current.childDirectory('../samples').existsSync()) { + samplesDir = current.childDirectory('../samples'); + } else if (current.path.endsWith('/test')) { + final Directory parent = current.parent; + if (parent.childDirectory('samples').existsSync()) { + samplesDir = parent.childDirectory('samples'); + } + } + + if (samplesDir == null || !samplesDir.existsSync()) { + // If we can't find samples, we can't generate tests. + // We'll add a single failing test to report the error. + test('Samples directory validation', () { + fail('Could not find samples directory. CWD: ${current.path}'); + }); + return; + } + + final List files = samplesDir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.sample')) + .toList(); + + files.sort((a, b) => a.path.compareTo(b.path)); + + for (final file in files) { + final String fileName = fs.path.basename(file.path); + testWidgets('Render sample: $fileName', (WidgetTester tester) async { + HttpOverrides.global = TestHttpOverrides(); + + // extensive scrolling or large content + // 2400 / 3.0 = 800 logical pixels wide/high + tester.view.physicalSize = const Size( + 2400, + 3000, + ); // Increased height to prevent overflow + tester.view.devicePixelRatio = 3.0; + + addTearDown(() { + HttpOverrides.global = null; + debugNetworkImageHttpClientProvider = null; + tester.view.resetPhysicalSize(); + tester.view.resetDevicePixelRatio(); + }); + + genUiLogger.info('Starting test for $fileName'); + + // Use synchronous read to avoid async IO issues + final String content = file.readAsStringSync(); + final List expectedTexts = _extractExpectedText(content); + final List expectedIds = _extractComponentIds(content); + + // Parse sample + final Sample sample = SampleParser.parseString(content); + + final Catalog catalog = BasicCatalogItems.asCatalog(); + final controller = SurfaceController(catalogs: [catalog]); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Surface(surfaceContext: controller.contextFor('main')), + ), + ), + ); + + try { + await for (final A2uiMessage message in sample.messages) { + controller.handleMessage(message); + await tester.pump(); + } + await tester.pumpAndSettle(); + + // Verify text content (warn only, as some content might be hidden in tabs/offstage) + for (final text in expectedTexts) { + if (find.text(text).evaluate().isEmpty) { + // print('Warning: Expected text not visible: "$text"'); + } + } + + final Set ignoredIds = _ignoredIds[fileName] ?? {}; + for (final id in expectedIds) { + if (ignoredIds.contains(id)) { + continue; + } + // We use skipOffstage: false because items in tabs might be offstage + // but present. + if (find + .byKey(ValueKey(id), skipOffstage: false) + .evaluate() + .isEmpty) { + // Fail if the structure is missing entirely + fail('Expected component with ID "$id" to be in the widget tree.'); + } + } + + // Unfocus to close any active input connections + FocusManager.instance.primaryFocus?.unfocus(); + await tester.pump(); + + // Pump a SizedBox to dispose the widget tree + await tester.pumpWidget(const SizedBox()); + await tester.pump(); // Allow disposal to complete + } finally { + genUiLogger.info('Disposing controller for $fileName'); + controller.dispose(); + + // Clear image cache to prevent pending loads/streams from hanging the test + imageCache.clear(); + imageCache.clearLiveImages(); + + genUiLogger.info('Test finished for $fileName'); + } + }); + } +} + +final Map> _ignoredIds = { + 'settingsPage.sample': { + 'deleteConfirmationContent', + 'confirmationText', + 'modalButtonsRow', + 'confirmDeletionButton', + 'confirmDeletionButtonText', + 'cancelDeletionButton', + 'cancelDeletionButtonText', + }, +}; + +List _extractExpectedText(String content) { + final List result = []; + // Basic regex to find "text": "value" + final exp = RegExp(r'"text":\s*"([^"]+)"'); + for (final Match m in exp.allMatches(content)) { + if (m.groupCount >= 1) { + result.add(m.group(1)!); + } + } + return result; +} + +List _extractComponentIds(String content) { + final List result = []; + // Basic regex to find "id": "value" + final exp = RegExp(r'"id":\s*"([^"]+)"'); + for (final Match m in exp.allMatches(content)) { + if (m.groupCount >= 1) { + result.add(m.group(1)!); + } + } + return result; +} diff --git a/examples/catalog_gallery/test/src/test_http_client.dart b/examples/catalog_gallery/test/src/test_http_client.dart new file mode 100644 index 000000000..0a6e63f5e --- /dev/null +++ b/examples/catalog_gallery/test/src/test_http_client.dart @@ -0,0 +1,570 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +class TestHttpClient implements io.HttpClient { + @override + bool autoUncompress = true; + + @override + Duration? connectionTimeout; + + @override + Duration idleTimeout = const Duration(seconds: 15); + + @override + int? maxConnectionsPerHost; + + @override + String? userAgent; + + @override + void addCredentials( + Uri url, + String realm, + io.HttpClientCredentials credentials, + ) {} + + @override + void addProxyCredentials( + String host, + int port, + String realm, + io.HttpClientCredentials credentials, + ) {} + + @override + set authenticate( + Future Function(Uri url, String scheme, String? realm)? f, + ) {} + + @override + set authenticateProxy( + Future Function(String host, int port, String scheme, String? realm)? + f, + ) {} + + @override + set badCertificateCallback( + bool Function(io.X509Certificate cert, String host, int port)? callback, + ) {} + + @override + void close({bool force = false}) {} + + @override + set findProxy(String Function(Uri url)? f) {} + + @override + set keyLog(void Function(String line)? callback) {} + + @override + Future open( + String method, + String host, + int port, + String path, + ) async { + return TestHttpClientRequest(); + } + + @override + Future openUrl(String method, Uri url) { + return open(method, url.host, url.port, url.path); + } + + @override + Future patch(String host, int port, String path) async { + return TestHttpClientRequest(); + } + + @override + Future patchUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + Future post(String host, int port, String path) async { + return TestHttpClientRequest(); + } + + @override + Future postUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + Future put(String host, int port, String path) async { + return TestHttpClientRequest(); + } + + @override + Future putUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + Future delete( + String host, + int port, + String path, + ) async { + return TestHttpClientRequest(); + } + + @override + Future deleteUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + Future get(String host, int port, String path) async { + return TestHttpClientRequest(); + } + + @override + Future getUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + Future head(String host, int port, String path) async { + return TestHttpClientRequest(); + } + + @override + Future headUrl(Uri url) async { + return TestHttpClientRequest(); + } + + @override + set connectionFactory( + Future> Function( + Uri url, + String? proxyHost, + int? proxyPort, + )? + f, + ) {} +} + +class TestHttpClientRequest implements io.HttpClientRequest { + @override + bool bufferOutput = true; + + @override + int contentLength = 0; + + @override + Encoding get encoding => utf8; + + @override + set encoding(Encoding value) {} + + @override + bool followRedirects = true; + + @override + int maxRedirects = 5; + + @override + bool persistentConnection = true; + + @override + void abort([Object? exception, StackTrace? stackTrace]) {} + + @override + void add(List data) {} + + @override + void addError(Object error, [StackTrace? stackTrace]) {} + + @override + Future addStream(Stream> stream) async {} + + @override + Future close() async { + return TestHttpClientResponse(); + } + + @override + io.HttpConnectionInfo? get connectionInfo => null; + + @override + List get cookies => []; + + @override + Future get done => + Future.value(TestHttpClientResponse()); + + @override + Future flush() async {} + + @override + io.HttpHeaders get headers => TestHttpHeaders(); + + @override + String get method => 'GET'; + + @override + Uri get uri => Uri.parse('http://localhost'); + + @override + void write(Object? object) {} + + @override + void writeAll(Iterable objects, [String separator = '']) {} + + @override + void writeCharCode(int charCode) {} + + @override + void writeln([Object? object = '']) {} +} + +class TestHttpHeaders implements io.HttpHeaders { + @override + bool chunkedTransferEncoding = false; + + @override + int contentLength = 0; + + @override + io.ContentType? contentType; + + @override + DateTime? date; + + @override + DateTime? expires; + + @override + String? host; + + @override + DateTime? ifModifiedSince; + + @override + bool persistentConnection = false; + + @override + int? port; + + @override + List operator [](String name) => []; + + @override + void add(String name, Object value, {bool preserveHeaderCase = false}) {} + + @override + void clear() {} + + @override + void forEach(void Function(String name, List values) action) {} + + @override + void noFolding(String name) {} + + @override + void remove(String name, Object value) {} + + @override + void removeAll(String name) {} + + @override + void set(String name, Object value, {bool preserveHeaderCase = false}) {} + + @override + String? value(String name) => null; +} + +class TestHttpClientResponse implements io.HttpClientResponse { + final List _imageData = base64Decode( + '''iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=''', + ); + @override + io.X509Certificate? get certificate => null; + + @override + io.HttpClientResponseCompressionState get compressionState => + io.HttpClientResponseCompressionState.notCompressed; + + @override + io.HttpConnectionInfo? get connectionInfo => null; + + @override + int get contentLength => _imageData.length; + + @override + List get cookies => []; + + @override + Future detachSocket() async { + throw UnsupportedError('Mock response does not support detachSocket'); + } + + @override + io.HttpHeaders get headers => TestHttpHeaders(); + + @override + bool get isRedirect => false; + + @override + bool get persistentConnection => false; + + @override + String get reasonPhrase => 'OK'; + + @override + Future redirect([ + String? method, + Uri? url, + bool? followLoops, + ]) async { + return this; + } + + @override + List get redirects => []; + + @override + int get statusCode => 200; + + @override + Stream> timeout( + Duration timeLimit, { + void Function(EventSink> sink)? onTimeout, + }) { + return Stream>.fromIterable([ + _imageData, + ]).timeout(timeLimit, onTimeout: onTimeout); + } + + @override + Future any(bool Function(List element) test) { + return Stream>.fromIterable([_imageData]).any(test); + } + + @override + Stream> asBroadcastStream({ + void Function(StreamSubscription> subscription)? onListen, + void Function(StreamSubscription> subscription)? onCancel, + }) { + return Stream>.fromIterable([ + _imageData, + ]).asBroadcastStream(onListen: onListen, onCancel: onCancel); + } + + @override + Stream asyncExpand(Stream? Function(List event) convert) { + return Stream>.fromIterable([_imageData]).asyncExpand(convert); + } + + @override + Stream asyncMap(FutureOr Function(List event) convert) { + return Stream>.fromIterable([_imageData]).asyncMap(convert); + } + + @override + Stream cast() { + return Stream>.fromIterable([_imageData]).cast(); + } + + @override + Future contains(Object? needle) { + return Stream>.fromIterable([_imageData]).contains(needle); + } + + @override + Stream> distinct([ + bool Function(List previous, List next)? equals, + ]) { + return Stream>.fromIterable([_imageData]).distinct(equals); + } + + @override + Future drain([E? futureValue]) { + return Stream>.fromIterable([_imageData]).drain(futureValue); + } + + @override + Future> elementAt(int index) { + return Stream>.fromIterable([_imageData]).elementAt(index); + } + + @override + Future every(bool Function(List element) test) { + return Stream>.fromIterable([_imageData]).every(test); + } + + @override + Stream expand(Iterable Function(List element) convert) { + return Stream>.fromIterable([_imageData]).expand(convert); + } + + @override + Future> get first => + Stream>.fromIterable([_imageData]).first; + + @override + Future> firstWhere( + bool Function(List element) test, { + List Function()? orElse, + }) { + return Stream>.fromIterable([ + _imageData, + ]).firstWhere(test, orElse: orElse); + } + + @override + Future fold( + S initialValue, + S Function(S previous, List element) combine, + ) { + return Stream>.fromIterable([ + _imageData, + ]).fold(initialValue, combine); + } + + @override + Future forEach(void Function(List element) action) { + return Stream>.fromIterable([_imageData]).forEach(action); + } + + @override + Stream> handleError( + Function onError, { + bool Function(Object?)? test, + }) { + return Stream>.fromIterable([ + _imageData, + ]).handleError(onError, test: test); + } + + @override + bool get isBroadcast => false; + + @override + Future get isEmpty => Future.value(false); + + @override + Future join([String separator = '']) { + return Stream>.fromIterable([_imageData]).join(separator); + } + + @override + Future> get last => + Stream>.fromIterable([_imageData]).last; + + @override + Future> lastWhere( + bool Function(List element) test, { + List Function()? orElse, + }) { + return Stream>.fromIterable([ + _imageData, + ]).lastWhere(test, orElse: orElse); + } + + @override + Future get length => Stream>.fromIterable([_imageData]).length; + + @override + Stream map(S Function(List event) convert) { + return Stream>.fromIterable([_imageData]).map(convert); + } + + @override + Future pipe(StreamConsumer> streamConsumer) { + return Stream>.fromIterable([_imageData]).pipe(streamConsumer); + } + + @override + Future> reduce( + List Function(List previous, List element) combine, + ) { + return Stream>.fromIterable([_imageData]).reduce(combine); + } + + @override + Future> get single => + Stream>.fromIterable([_imageData]).single; + + @override + Future> singleWhere( + bool Function(List element) test, { + List Function()? orElse, + }) { + return Stream>.fromIterable([ + _imageData, + ]).singleWhere(test, orElse: orElse); + } + + @override + Stream> skip(int count) { + return Stream>.fromIterable([_imageData]).skip(count); + } + + @override + Stream> skipWhile(bool Function(List element) test) { + return Stream>.fromIterable([_imageData]).skipWhile(test); + } + + @override + Stream> take(int count) { + return Stream>.fromIterable([_imageData]).take(count); + } + + @override + Stream> takeWhile(bool Function(List element) test) { + return Stream>.fromIterable([_imageData]).takeWhile(test); + } + + @override + Future>> toList() { + return Stream>.fromIterable([_imageData]).toList(); + } + + @override + Future>> toSet() { + return Stream>.fromIterable([_imageData]).toSet(); + } + + @override + Stream transform(StreamTransformer, S> streamTransformer) { + return Stream>.fromIterable([ + _imageData, + ]).transform(streamTransformer); + } + + @override + Stream> where(bool Function(List event) test) { + return Stream>.fromIterable([_imageData]).where(test); + } + + @override + StreamSubscription> listen( + void Function(List event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return Stream>.fromIterable([_imageData]).listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + +class TestHttpOverrides extends io.HttpOverrides { + @override + io.HttpClient createHttpClient(io.SecurityContext? context) { + return TestHttpClient(); + } +} diff --git a/examples/catalog_gallery/test/verify_all_samples_test.dart b/examples/catalog_gallery/test/verify_all_samples_test.dart new file mode 100644 index 000000000..8e0d91411 --- /dev/null +++ b/examples/catalog_gallery/test/verify_all_samples_test.dart @@ -0,0 +1,41 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:catalog_gallery/sample_parser.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test( + 'All samples in samples/ directory should parse without error', + () async { + const fs = LocalFileSystem(); + // Assuming the test runs from the project root or we need to find it. + // Flutter test usually runs from project root. + final Directory samplesDir = fs.directory('samples'); + + if (!samplesDir.existsSync()) { + fail( + 'samples directory not found at ${samplesDir.path} ' + '(absolute: ${samplesDir.absolute.path})', + ); + } + + final List files = samplesDir + .listSync() + .whereType() + .where((f) => f.path.endsWith('.sample')) + .toList(); + + for (final file in files) { + try { + await SampleParser.parseFile(file); + } catch (exception, stackTrace) { + fail('Failed to parse ${file.path}: $exception\n$stackTrace'); + } + } + }, + ); +} diff --git a/examples/catalog_gallery/test/widget_test.dart b/examples/catalog_gallery/test/widget_test.dart index 5117c55d3..ffaa32287 100644 --- a/examples/catalog_gallery/test/widget_test.dart +++ b/examples/catalog_gallery/test/widget_test.dart @@ -2,24 +2,32 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; + import 'package:catalog_gallery/main.dart'; import 'package:file/memory.dart'; import 'package:file/src/interface/directory.dart'; import 'package:file/src/interface/file.dart'; - +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'src/test_http_client.dart'; + void main() { testWidgets('Smoke test', (WidgetTester tester) async { final fs = MemoryFileSystem(); // Build the app and trigger a frame. - await tester.pumpWidget(CatalogGalleryApp(fs: fs)); + await tester.pumpWidget( + CatalogGalleryApp(fs: fs, splashFactory: NoSplash.splashFactory), + ); expect(find.text('Catalog Gallery'), findsOneWidget); }); testWidgets('Loads samples from MemoryFileSystem', ( WidgetTester tester, ) async { + HttpOverrides.global = TestHttpOverrides(); + addTearDown(() => HttpOverrides.global = null); final fs = MemoryFileSystem(); final Directory samplesDir = fs.directory('/samples')..createSync(); final File sampleFile = samplesDir.childFile('test.sample'); @@ -27,10 +35,16 @@ void main() { name: Test Sample description: A test description --- -{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": {"literalString": "Hello"}}}}]}} +{"surfaceUpdate": {"surfaceId": "default", "components": [{"id": "text1", "component": {"Text": {"text": "Hello"}}}]}} '''); - await tester.pumpWidget(CatalogGalleryApp(samplesDir: samplesDir, fs: fs)); + await tester.pumpWidget( + CatalogGalleryApp( + samplesDir: samplesDir, + fs: fs, + splashFactory: NoSplash.splashFactory, + ), + ); await tester.pumpAndSettle(); // Verify that the "Samples" tab is present (since we provided a valid diff --git a/examples/custom_backend/.metadata b/examples/custom_backend/.metadata deleted file mode 100644 index b25d10412..000000000 --- a/examples/custom_backend/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "e11e2c11288b6a3f9f9bb3dcfb9bb459a75e048c" - channel: "main" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: e11e2c11288b6a3f9f9bb3dcfb9bb459a75e048c - base_revision: e11e2c11288b6a3f9f9bb3dcfb9bb459a75e048c - - platform: macos - create_revision: e11e2c11288b6a3f9f9bb3dcfb9bb459a75e048c - base_revision: e11e2c11288b6a3f9f9bb3dcfb9bb459a75e048c - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/custom_backend/README.md b/examples/custom_backend/README.md deleted file mode 100644 index 2b7ad5cd9..000000000 --- a/examples/custom_backend/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# custom_backend - -This app demonstrates integrating genui with a custom backend without using -predefined provider packages (`genui_firebase_ai`, -`genui_google_generative_ai`). - -**Key Features:** -- Direct integration with `A2uiMessageProcessor` -- Manual tool call parsing and handling -- Saved response testing for development without API calls -- Shows how to build `UiSchemaDefinition` and `catalogToFunctionDeclaration` - -**Key Files:** -- `lib/backend.dart` - Custom backend implementation -- `lib/gemini_client.dart` - Direct Gemini API client -- `assets/data/saved-response-*.json` - Pre-recorded responses for testing - -## Getting Started - -This is a standard flutter app that directly calls the Gemini API. You need -a Gemini API Key. Get one in [Google AI Studio][ai-studio]. - -Then pass it as a `--dart-define` when calling `flutter run`: - -**Run:** -```bash -cd examples/custom_backend -flutter run --dart-define=GEMINI_API_KEY=YOUR_API_KEY -``` - -[ai-studio]: https://aistudio.google.com/api-keys diff --git a/examples/custom_backend/assets/data/saved-response-1.json b/examples/custom_backend/assets/data/saved-response-1.json deleted file mode 100644 index da064b09b..000000000 --- a/examples/custom_backend/assets/data/saved-response-1.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "uiGenerationTool", - "args": { - "components": [ - { - "component": { - "Heading": { - "level": "3", - "text": { - "literalString": "Here are some of the things I can help you with:" - } - } - }, - "id": "heading_1" - }, - { - "id": "multiple_choice_1", - "component": { - "MultipleChoice": { - "options": [ - { - "label": { - "literalString": "Show a cat fact" - }, - "value": "Show a cat fact" - }, - { - "label": { - "literalString": "Show a dog fact" - }, - "value": "Show a dog fact" - }, - { - "value": "Help me generate UI", - "label": { - "literalString": "Help me generate UI" - } - }, - { - "label": { - "literalString": "Answer a question" - }, - "value": "Answer a question" - } - ], - "selections": { - "path": "selection" - } - } - } - }, - { - "component": { - "Column": { - "children": { - "explicitList": [ - "heading_1", - "multiple_choice_1" - ] - } - } - }, - "id": "root" - } - ], - "surfaceId": "assisting_options" - } - }, - "thoughtSignature": "Cu8TAdHtim+zFT7l4fQNJUxIYFT+LcJJmx6eR5wNEa/aVrfwJKD1Z/RtErElLce3AUk6B6pft7eH6xPiYZHstTE2qKXlA//hm5kV0XM6dVxJZNBml6oHWSMu8xHBOYoUHHFkiCDKj7UPG4iMYzzZQT5XMVFKGzQ3W8HIh2BsKWT4oCvRkdYBADmjFfGFb6nyWP3rNww1qHAHO/9JwHPxeozL29D2tUGR7wFA69iKJEif5Lmk2x02INzmTdOP51ehugYGT6qlMvF0dV87d54yta8mggQhSGk/DyPe0o5J1BXqwXcSEZNZvA2BZY+elXFIcHzrT7HaAUdIXsGnVxPFQ1e66TqHQamr1weTim28s2LyUs8uK1zgFw8bn6gx8a4yB5id2Dx2AfcO/R9cohw7ZxC+kfSxWKCRCm2asEwkNVGu8IlMe9xWF/2Gtj2IarTouwMgpbXwqusfIW3sWPFTsy6TobDg15fdW4LjQ6UORcoXpblg3jGfPNqwm9Fvx3S71kyMbf0icqSb1BzGNk9E2sjV389LFwLQvDZd/6C6vt545UUM0+cxBkaY54ib76rSt8d27g0SulJjYfqleF1qn1MIwLtYbZS5pTfNitvKiWnJ0Aqh1ut3NsvDUDqpNvq33TCpsZGgLieifOOyBfKHfg0IzscNTRTCIyjhA3XD7f0633ayH0UK0SKmb5eO2gc799NHFRqNAbck8tpyn0KknO1l39PFdRkcvTLQ/oSb0CfvE3BrwQvOCo1aQ91c4GJbQzRxHCLbYqGvGyhvrJwNJqjfR3Lfid4Mjcj9/ukNFQBSpPFiHzLCXgLl8xQa7SffH7G1gXGyLBRiSsACDriw7gM+HdM/kaRk0aH466LZGagPDFn5YWe2cph4qpAhyZzH1OQnMH8k11CpLvAxaEo1mGx4DXp3K7CQSjva0bO6K/CMm0n/wJaevWAfZuMclmPVRUN1iDiI6Mf1IrofSCYbgCQWMsgPwljD8GNlEs/wWHDNz3bJ5/XasIG/cI9mEQyOvNndCMCLmy9WbqGdKQvarXguAu7gl1cpCg2dRRSyY22avEUqH/pg5CEiqL1bcAPiKR6hOQECPES1kWmBM4hUd+UB9kjv+n2uSpePS2+Lr9ur5FjWYxu/XPSi/ZZ2irB4VoJO7/1e0SXpd1zZK/9W0DBRuPZzmYD31c6P/Fwp3xWdjl4mrxgCYKI7WMjLPYVTya3XS78Tn5mJ/Z3CH8uJe49bytuMVOf3tfSlOkfrNWqIy45svqF5QUqqOghybeNVAdffcZOS+JnEZj65Z0+5RxF/PRqCu7U4btnoSeVXQXmHb0xFgf6c8l/rdgiROTgb4OKjW9thoPu38Ql2KARVvOr9x9CPOwFDXIiUENmvIa8F4RGxf+HRQjYKi6XLUbH4dqm+4xtNUZEzPMB0eeN+tI8IumHvc17V8Z2ztTywKv1PgAMoThmlBPmDDQgq00tJR9/S8VPROgbZmHvo0U1dIbE8M6QkOTSD5bQsiMyHzKv39RxHoilNJmz16Rgexjn1qV+TbQOxmphQzCrVgc8GfzQDSQ0YPOX1H+QFYuDmoNF3Cu/ndqiHPtklIJgt7nphGGWXQjbvB/CVZENb9RYIOaICbnDAjZ1cHeH/kzoMieyY3zXr8vQZTIYBEldRKPsqncoLNeIZZLYDZH0uhI4ZE6HLAGeuka9ZmnMFACvUIX3SllOkGEPAed2MI9YYdIkWTbEjFpFSlZC52jgIVw+3J853oyun61M5XgZ/6/6D92BjYhD6wNmmXyePUTy9MG31ZJikJ4hb6EJKFeAmPeHXXLWaj7sH6BUinZHl8otUfyV847nJxGwudrBWfJFY7CVW5CsSrSiZEhb78qxRtzp1zRu2ZqzW6LLO4Efm/6FdjZ5QhjkeZqaZPFEfx2Q27ggLcyEJYs6yY7DeB8WBijGESHbT3CNJ7BihrJKeNwBNwo1xRCX/c0M2k/Sp15OW+aF3tcWXH5LqS/sNnDqCK7rJOoZV7SS4Amf7aU4fanwcyhxTUZzVYoUJDWrlZtNOd0E0UiDbLuzfs2YIEAb2bHFr7iFOIqVriroqRtpPIVGy0ZLzXd7y03+nKhNm7bsb36xenr90RmzTRzUFBUz/xsdmBlUKlwjdPJ/PI3f2rwUQOWYcGbrJ2wBI5oErJ6A+sugWGklHUttW6Hw3A+jGBmOhletcK9/uFNEqKWK5YJPPn0aJQYId786x1vTuI/Viwbn2UeAOgsjONy0EcV7HD4AAjYbMouO++9zSWFNcFfoelExFQKgdb2swH5OH1L6Kdb3Ibi0tI3Zpbsp5fVk9XaRrlhHU24kfUg7P3zotYnv+mPxN9xcrwN1CXUQscOXvvhYExnktAmvhQ7SPa5HFcUH2V3RsKtOKe4U/XTq7moowP79AS+Db3fT0xKUoSo/DVmcJaGQfL5TwkxhJv10ebFiNYMH9BH/Sp91QplmO7GhXUQfZPpSIKmtZqI+d3o5UPuzZ1RMlPXHdsNLlmcE8Mu2/t/Gs9fLhQ0sjfIvwicaZegPR3ZrFEXsHABppXgY6jFvMpFclFqYQbqPu5vEXLnMQdO6XmMQ/dQPmkFeenMzEygwsApCfdX+lesnO7FgUCaiV74uqZfRQvqUnqos+oeaw2x8dJdaHj6RycVRG0e8cmeyhNPqVVyM9bZzL9fkqAlyDBLy7rcOEmYNQeu2HpQH6IN7/U0mgF1NreCYnuVlyT5HX+DCPjTxFs8K81EtcTmCH037iT6eL1BgZ286sPr1JW+2HxxpqUgK4kjiwRGJQ8ms9DK6z2rUisfpQmkbNRhl1RlM3F3l7ZYpuOvQlf0YRF+jf0XbZtjJM0Fu4kuC8Po1PaQvzZk3ocbpOeAn4pFtfrjB07mkEIcZSwbjovrsu67oCwtOUiuW2zosdRR5tQbMlcCVNuxn+BSO8B8qtIaIIHBbQ0i1r7zCconwNzSeOrHct0q8dCRPHZTtgXeXkqmjj1Bwl8BoWhtHtkxFFg5zkOeY2Rfn9Y/FmtO/fmWCayTmuyDEGawhx5PSmHdIQcePskAFpz9SmhQga7nWzVPzGIC2SpGc7hBHTRz9at15/fULCN5xh7iwtuWz2/eVuhX2ggmZSmna5IpHT+mp3SQ8TqvOS5qzQSFgu0OxJjzWa4J1EPnqz+7Gn7/VYNYg7hsEcQNhnaFOOQkEfYEbFeDLGwsdmLiiBnbxdw3/bJvLUw3zOE/uCCDBxsoLlAo5bDlXT5JXvEvuIQ5Z27Xr/1SOJqI3pNO2EGrxPemxGZbd0Xe3vC+GLK3UMVQ6osHIRRAHgwzGyo4LExbON4Bqfkxxn/AiuDQXtoBOiXS+iEnEDOxywTvzz9k6Ao4ItCvz5gqRiQdxzPuQCaITPB8zYV4PcP4Q=" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "finishMessage": "Model generated function call(s)." - } - ], - "usageMetadata": { - "promptTokenCount": 4568, - "candidatesTokenCount": 213, - "totalTokenCount": 5480, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 4568 - } - ], - "thoughtsTokenCount": 699 - }, - "modelVersion": "gemini-2.5-pro", - "responseId": "_Yr5aIIDyZ6q2w_q5Iv4Aw" -} diff --git a/examples/custom_backend/assets/data/saved-response-2.json b/examples/custom_backend/assets/data/saved-response-2.json deleted file mode 100644 index abcfb8acd..000000000 --- a/examples/custom_backend/assets/data/saved-response-2.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "uiGenerationTool", - "args": { - "surfaceId": "helpOptions", - "components": [ - { - "component": { - "Column": { - "children": { - "explicitList": [ - "heading", - "options" - ] - } - } - }, - "id": "root" - }, - { - "component": { - "Heading": { - "level": "2", - "text": { - "literalString": "What can I help you with?" - } - } - }, - "id": "heading" - }, - { - "component": { - "MultipleChoice": { - "maxAllowedSelections": 1, - "options": [ - { - "value": "answer_questions", - "label": { - "literalString": "Answer questions" - } - }, - { - "value": "generate_ui", - "label": { - "literalString": "Generate UI" - } - }, - { - "label": { - "literalString": "Write code" - }, - "value": "write_code" - } - ], - "selections": { - "path": "selection" - } - } - }, - "id": "options" - } - ] - } - }, - "thoughtSignature": "CpQaAdHtim/IzB/ceI96yONME5/BwhAh37TcAzro5uKtmFjBh/71KuXjqYp+8SZFQjYXgjCTp4nA448drYqgnVjuPEfv696HxkwBnO79OzRWSRaSZ1vX5qNNh2Y0CuonLPjJXxR4cqi5ZNZ19gyWGCSU7rDBBOjXS8AyogYo2mnWiKlCHhWM6qe5O7Jeg1/VuDc2XPURLjoFkrCnbOEjTzr9wIdsSmS35E+raql/fg3EGr7jecQbhsvXSwLCsZOReLw/V+J8BRB5yk77mfu8LsFWOsdtiOcc7JUlHgLfGkCI74vByYhiy0DiBtC8gfUVJDDBBn4fWJwdUZSF47Nq1S6ecSnBQO+mLA747c2/AKntVK8P1FKeHwgGfU+kW02/vtMyLQzDttnHsbXuVtEhQjC2XDflyjO4ZYHY6fyxvlVHorC6DVzf3cfSBSURx2JZqB0MWQVC+iGPNTnFJv9q2p1YBFyKaaxfOz8/WeQ8XDlAvgaZGTTmcVh4uAS4ASy2XOc7xqcio3ti8NtLX6ivy5GSf4laI6VKK1oM/m8uKeauN+cD/DD8hN5ntBwkHLStqtW/jms6/Rn3nWH16aZUIJMnBH1V7DOBIMk3k4Mfo6QAzQTlUfhnWGZwK/3LHv2JohS9JiBRWjSOwHi19f43J4IcwQdcRj7dKjsbg9S+I5XkIg2GzU/Wep7L7I2oiqf49doBcEfFhkauNqwoTglpyBKFN0vNZ5qig2EgPSQvZtiIvyV/rFMk9F8J5kKxavMb7Abe95LhAKRk1NKJ8zJz4KuB/CL/Nzw1Jf+jAyTvBUCOzIphq2IQfZaQ936hrDDjiLCpRefhn//uN6hrQ787u8uoZlGSk87EUnDF0cJbqaKgKwmGHUZgLFPYTmPdFHlW8cGXQOzTh7w6GfK7a87bo5IJxAIQzqmuLpXLub8fVZv1VvN060fBwz3ZKKEVXlmG86xR6HEcb6KSZhPdigNjzJ26UnM5kC0s/WcjUsyqWhc8iIchRJOaj1I8xJdcHOYqOAlj8L7Phpa65z6Qgj8dWuub9NEsjaAKSQ47Dqul/BR8841WxSmZMjUqIZOWDitbHzc2q4gTfDcomvRYLQ/pnTZkPgImJWE42L1Htf1wcXdrC9t2TFyRGplo9vKpsIUhqsvAweRazBPlPTj6rmz4jYWRKUuPEiNOLsYVAzs7KGhO+GnPM2D9tlmYGXN1e0S/cc2p0ib/p+m8FkM+18WI+fprxgmMSmVKQC7uzxhCfu8uqDFiLE7djFprHpf3iBdxcIi0IrEFK6vAoRTk1gAD4mmLoauGzV1Y0isOfZHIjn3sMCnyIL9Id3adMHgHzU038uWvn4dZzAfsa2RQ059rQctDwGDmIy7bjcOLVqKb7J2HkOAqBoUa+JRj1hBlHaX0+vPWCdULgacBpS6Z0ngJfHmRihgdPJJnMVfrT0ROV4wFDiEF8ConTKZeetS/+sR04co/HYk4tDBSd9d46Ad4hxKGqGLOFyCGHjNBP5MtFNhI/Ilpa6yFvEntpdcv8OECO2phHT6/CzdId9jG1qKPAcoO9jOAP8wouqinJB8kQXUQKKAHwmVJp/t6SR3/r94bb/PQqn00z8pUWzvT+rwkXcwI1uZIUv0cfoj7c3Z+1t1HMCJ8DTspOrd6F0HukZ5NnfjXcq4aSfLhox1c/UIG0zAC6r51lg8jh37+hk+SsMPKVgfLbDmZR1mxsRbbFsTA1/HgFTGsdS2jfu04w0wua7tl5XUGbf1Emwd1HjO0kSsCNMDR99ppMY8o/kZbMtLSbl6m6mzB89QlzJt9AbWicTkbyzIl03Wh6trZ7kQEkDWkS4fq8OofGwfibt+3jCM2z8QcIpMUjiZ8kx//oWkz0S2GXzuY9XeCGN2U+Lc2jsEUv7KSU02Lsd3lc8jC9+lbMAxX7vLTqHbfjGVFC3ko3QonK8syRL1dEVxVcnvClXty4Pvw5Bjut7BHT+NlV5A+x65lT7IPzZvN4AXPqVPYjsC7V/kR4oPailHs4NE4BhtJJvfSDu3e2czjkE7968QXRchjAb0c3hyLWF0pOMZ6SfDArDvcx39wnkLNv/x+kMjVV46KIBjlQMiKvR7vfuwTO55Ccy+qmPlDFLo4aLSsGuywXyAGjLXtwQQsd3xXiiwkvdvG+ATwz06UKliNZTt0qdtoXPh490w4h+Ib+tcOVOJ/Ncj+uH+llNSGWZuUPgGkaWCRR6B5TmgoQDcoOcWSnQjhJMiVkbSBLrxX69XdiWIZFpkJNFAKjqlCcIJVovUxnHjixBUxYTBcQwlBnu4AUTYnMA0QJCYQkT4JTGN8hwbz5olnRfFrlVOZXG87cW9WA0Z8h+/si+5/Q3fTx7Yt+SYPWgnCheUXq6CnY1JOlLTqFrGNapwz0UWUPYByVXHCKHk4kPXou5j5A9ZNGA+g3hq1ThtnQA0unda/99WZeJzFRHb7/wWWA/M1nFhho+gKEwrF1/avzDjCP7J/SdSHy6Tl1kO9/XzKJk6qp3nTQ/BKUB/NnlJdnYbpa4Wd1m23vdzQkxBEyJucgqlU+BGoQfPBL+DEMwnJNA50Eq29HUE46fnQ9Kd6AXWn8J1gVm16vfbA/LLnpLTGg4B4mu7YWFJuXzeS+7c1V8BMRCwlNMNgD6uTcFnj0CAt0bNZRfyRK6nmMdh7o1jnydssNMt2gTMSjLRZFcjUJuOmPuVz4lZS9SPmakjM5fsTsu22PkdmOJIH0dWMIxDE+EI7QU2PET4XyaWFb9BKHdt/jslsowvhEEnXQekkOrQCYoysFdFrpCH37V2xRZG9nJpWNP3HVa41LUQv2FMpx35dSjT7O66cTj+oqozTa3YmrrUOGpNeDv4B6xW08blrxXC4geyEMW0DzJByhNWLSVfCWdr0X3Qs/xGoWcSGOumvdVfib29fUkktUZpvgky+Na+DP5siKL1NYwv6k02mZw+TOHPP4dFcCnOulIvA2+CB1Kz8hHuUJntwfdR7H1SCxLJBUIOamQUx9X0aGhV6YQ4G5IoIIzSnfPtFw6W5ak879wDY2AtirRMXnFzAxFEk9LQSO9+tLjwV4VYdfUdb1QuJc9dhmIFO4FCL1H2gv4+rsx9YqSkRMU09AwwDqnRUs27c611qgkviZjoFBMbppNriTlzAJDQsILWNeOorirdXtNvJuKrCN0AL9HXImZVWYqBgq64gWM2XgaKYpygbOi366zqK2wTgiV8DfpgnPqK31a/OVGd/kfqXUq3p5QZ9xjeYifPK7Y+dlmnZVpiaRyxBUmTKlrNT0oOAn1Ha1LPkrPZq0sNs/8xi2U2S/oQOJQH6dIMZRZnYMmjyuntBfYTQ/HbATUR907bR59FMyD6xZg5YN1gsKn6k11Q07sDBoY9ETCMy/h1y5swTnqViyHVMfozDmLs7dIzDoq+n1UxJTUoppe1+BeLDpPI2gDlZJ+L+0AQDqF19vBAAZHnxvLpN63xefZw20R0FGWSnJ0758AskL4GqdUipNKbXgqkVrTvwZcgy7ZNjRzJhv4Mrq+wNuaj5Gb/gWePvVaVWQDqHZtf9kHR6DLQj816AM4uzLpuK38QNWm7uY9Zahj/nbE0UAgWbhPnaIhRl8rdXXzOsX87LgWopZpdIiAQHCllMw/cgYxINSxQQrGHri7+aq02UmBTGEEGWWt2D51veiCmnbk6Z8ZHU75/ls14AT/LGQbU/Q7GZ5BQegEGwerLFPt6Dw89g7ZOOPwbw/b+9uneVh703fBgn3T/IAFSGtX6cDOR11Sa5MEp8X0M7Sy8jzbOkjkdq/4UAhOSqDyc13IpA8xa0WhU2bxGeJ8iiWKrhnRjqvPDIhRkQ4SJDePqqO+15IoreF3GfJMiIn4lDEH/f+iUJh3IFIC5Ax+pnNylUNUElaj2MKVceZDM89Oasf7ClamoxldlLVRqoUC3ZIWv2rWvsvPak5TTQE+PBmA3k+d0otBcZAM+MJzVEzTv81HJz/2tQywv7i3nyNVSI6T0jnUurRt2FUa/sCnqcfKNbzrCA9ifbFzN9VliT7No16cFNIkj0HD4OCKxz0j+uHJYaJVmpC3C/eny2JQA6HIBbKx4cefvdsA2wRwH4H3w/dIWUJmpwfLJrE93L0qqegjV9kFnKzxus3vr6yxA7i4uT/LTo83P7/ru53YNbthMa23IF0aqruZUfuXdWbcsplal8RXxmCPs/T7FCRWfNJ6i+ouk3ds4UtalBJN3pgt89/t9NYVzReI4WsgxcFr4o2iGFyGIsQolQ9BKsh8yDZm8SuF6iOloAOPvSNRb8s6eKxfMLVfG9kyrbMtfgJM9sifC3YMpSQhzQhszqxiFx4v4Wq6Psz5cLuKN1O10M74gJTgWOevVeDVoQaRcv3HiONdrcRGAgMU/63ite03MTFNHANZMaf1wcUTFj9CdESozg+18Oy9EEZRB4UCuNDogzy9JlTv/tLsHOmf/WQggI" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "finishMessage": "Model generated function call(s)." - } - ], - "usageMetadata": { - "promptTokenCount": 4568, - "candidatesTokenCount": 173, - "totalTokenCount": 5666, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 4568 - } - ], - "thoughtsTokenCount": 925 - }, - "modelVersion": "gemini-2.5-pro", - "responseId": "nIv5aLyvC5C5qtsP98KaqAs" -} diff --git a/examples/custom_backend/assets/data/saved-response-3.json b/examples/custom_backend/assets/data/saved-response-3.json deleted file mode 100644 index f56a70aae..000000000 --- a/examples/custom_backend/assets/data/saved-response-3.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "candidates": [ - { - "content": { - "parts": [ - { - "functionCall": { - "name": "uiGenerationTool", - "args": { - "components": [ - { - "component": { - "Column": { - "children": { - "explicitList": [ - "heading_1", - "multiple_choice_1" - ] - } - } - }, - "id": "root" - }, - { - "component": { - "Heading": { - "level": "1", - "text": { - "literalString": "How can I help you?" - } - } - }, - "id": "heading_1" - }, - { - "component": { - "MultipleChoice": { - "selections": { - "path": "component.multiple_choice_1.selections" - }, - "options": [ - { - "label": { - "literalString": "Generate UI" - }, - "value": "generate_ui" - }, - { - "label": { - "literalString": "Answer questions" - }, - "value": "answer_questions" - }, - { - "value": "summarize_text", - "label": { - "literalString": "Summarize text" - } - } - ] - } - }, - "id": "multiple_choice_1" - } - ], - "surfaceId": "options_surface" - } - }, - "thoughtSignature": "CsIWAdHtim9Zo+PI7k0W3OowwM0OAhT4TamHf/Pu9TFlYvhnOZ6XWP/Cy32UH47t1bmsGME4IHnYrWVLfCUH032YNS7xge/si/Vfemq/PeeqpSMC7namgFPcmXpyuLHpvGLaPWXTHjVq8oJ8SfWIth9G4nwoFOZYtiVoC+Ftaphcc4jZ/GI3ZGMWi9fFIaLlF7HNskFZRYsqSBiZYHh8i44TJRP4QZOHC+H7iG7FJK929c/m5ApSLdUxhIgkiYDl2kJOUD94NoVP6ZS2fe0TM4Khlmg3JSfmgSiHcaeIcB+pJ/bn1rK+Il99SAr3g/DEV4HHPqRUxzcxACFpvQb1tKsHbX2D2Rf4rfDy+RN+uJpasejhNM2i/NrZwfF1Gn8DDWFpSpW3Q+tJBopu+LNk3kQqKKT9qPc/JCu6Bj9ZmFyUtrqajNogrTSnkvrbmwewQcguZEtsriEK83Mfoa0//YBN6q2eelbIi1st4VCHjN7aCYpvqQ+SUR290AdC24eS8t6dRsLBN/U5XDliDtW6Gh7e+iEugX9WTg6yZcrWrd5lXY5pXiG7EaDzZlN12+CfzN7BkYEk1/w3ZnS/gt4v4Lp1H+GUxd/KSxOq56uTjEzmUWf2YZ2ANl2GRUR/Cy/qbu4UA7lN2+NZDIXW0X/Dh5f3zxi7v2mM6OkK8V0rxIALLqzVj22eg78JkDN584ei33KL1nWFmGJrlhIsbJssbOQrcDyXNwzSmj5Izd6eByUGRDKpGwyOxHVrBtZHbcPn8nV0JRANNZYB4XdCu8N6EQOztmR4FdNSBHuYlWaMFJwoh2xUSIN6FyXK7fra5Khl7lsrQ63kCDmZZqEhggxT7WOiw5w07qD/+yMXubXdLFRRvdZoSNF4h7GB+557LE/1c46IImxbo6qnCUuaWhnLXoxU+tYD/rYugtD9KcWr1KSQRID7q+GXsQnah+pxBOmbHMWs2slRCZTfPPUHa5t49gGwHN/Y4iZDpwU3yAEYhrMzhzVVo8SU0zVUfisg8FHd1lb0bJvyLQkX4IwWd7dtN1TBU+p1AuhSPXLnEPlRRF8Jv6hv9t/pI1fQDCJiTxAWHUtt7cLHNsnjQ7m2OE/JdJ5NJkVNorUVR2kTG29AggQJRQs0vk7RjJ2rikJ8I7tlaRKjhIjJGCHiSUjsgkbX1XsihxurJr3xgUiQ99HYukWnIhY4VPpj+zHoJy472FifExEPXfqnsV/cxVy6xlshBEU2hz2ZtxZSTPRUrYeNSIexnSweV7d5fzRR7h63RXc+wkLqTYEi9DsK1vlWLzNlVI9YHp+fi9RiTps+fl8gaerGR/dagtcVvGOZ7pJ+X39pBkDNnPh8eIY7bY+E8OJ9UHNq2L25g5+5V+VGLjOnzJq1qaPEVtp+WljBkQ8agnhf5fCZy+yoLf+579fGFLtmBdR/qi1KN5K+XuOurZt2mUszuUDEw/bNjdRCFgCqvmWR4nEW/pcVljKuoq3pnbiXT5LkhPU6wmDTZOu0FDYi/WqsepAhgvyKThM4MBAumuQOzwwHUMnfhZSkiQ7UohRLBgWD3RO6hc1YYeZFrgkXdNNB+njU2I1C4rqzWMnrFHc/g9vbT98hqS4OQ4JO7xtbJdMnMxLp5nRcKLNluB0M0/ZJnCwz6qVRdCgoWj4iZntDpqkggnacCyHir13QjLwD+Ow+SmsXJXmPTRQkghVjs93PQqGwp4XLSyhr2jJwPt47dqMZbGTZ51U3T2MjNNa7cN7PYOhAQDZF8wHC016t8KGxrHiK4lhtscPSwTu7mDHBbaqvUSE2rg2GBnSp9mpuQYLYXXEoIv0iijxUWQpFUWZZOF59ig4oO+RBJD1P8KuBTW7i8ay3ayNRTW/7VX9vatqn1xRPP2FfAYTjHTRbC8Gq48Cr4WzLGY2BqBwcQzuhUQUf2Jr7WrLlnsNjGSKeu1P4H2bm2/Q7B+4rzq+U1zinlTEZN6kKDGnD3kEZaf29F3i2zXZpIt7G5NStkOQMQPQz4kgr+TES3VzXuhhFD9IRlUXLYXJimJpJWdUMEWDrRFZ/1eQhrYxkoH5br1fCL9DWvSSQhpQtXxlYHHza3gVM2dF+HPMpOL359kJgWavF7kDUpJEBUKvTQ02OXij5hCUST0Tx6DVoQGgjIuoqbd7O6o6iJHOrTAkyvwhGAd/rqYLPmVjCoevUtMz1gCrWiDxXRVS7nOEP2XsKKELQn7OmRP/r3c4CvQcfkeZHYs4YK6AF1FxMFx493qy540CEvi3SQZjHO7uZZfb/OODeVEHFAEf5fD0THzapX/QZHv+GJ0VD0TMUwvi7efz2DDSZ3XDfq+vhFmjMx57pNGhf0bpKodPqsiZ0/yQU643CQ2u3Yt2cOvmgF4OPq3SH1XMtXhyqhBduSjm8gPy98SpPaMVQlyZSRW66UXtUgPqYuITrw/KQRdK4a/Ax6/MFilF6cOEjCfvWbpDbQoESTL4/EZHxf4k7tpfbpE7LdTuGcZPUlC2tMoWdcHFRKnb2mFhwf/dZShXltN4/6r5py6+5HnnpiXxk0Msvydbsiz7pkzJ1996obCwZpKdR79DVqyRw4l3nZQ4FF9H6tFFaYxXXoIFJa02B1gOLXitZ2dP7xxGaLZuGCwR0Xen8/Q70yZ0gBkSQZOFVWdBsokQhyvN6C42I68IrL1zad3qEvSsnQhSYHiKQbXc1la5CLU2TTAU7d5OD25Q5CYC1BQC+oVzkwXOqDAbuapDaFVIxsxgaIPUYS+gTSWVh00yyDGNWSsfPoGXLQVe7jXEnEZQSDuWQuHTH+BWm/+HE0fo6+u9ClrrEJwBbyCsjRvIHNlx4anuLO3fO6VtNohFm0hLttWZSK3Rte7Nt8MRhnMdTCUBx2vm3ERHtd/1d5k1m+UUkNlFSk5KZDhp8K9ZASCu7jAqXoNEvhNy/8g/nFmoF7qp/cVBFRoX9gs4Lute+tvcyD7JKK1Z/na8ZiYxDtU1WtGa8DL+pklcUT7y69ldJyrgj66pPb3IZ0bBa7lf/wJqy4ZZtIacavuJnTGGzTBO65xxK0Yd3boW2d0uWkKb4tZnZNO9ZkB1h52hIlNefYLqrX+6t9zWYw0oXs/kvdobOmHpFNDdmWS3RP9SmKxdeFiqadrK332Uw+oYL7P6w/0GWVOQm0Ug0qqTCd7FrOCpdpS34LQHvYiNj0eLptYzLRlIwUJ4y76S01HUAT8v9MAGJ/gjodBdjMk0UcymLdN2PieRbTjfNKP9z2vHr7UGToDhY5f4qXO98v4Ws1O4WBUwPkR8D2nLd1A+IHxTTdzk1Eh5RDJejMtAAJoGp8YD6Dhm2MInljdsNTSWdJqdDdRC/z8TZXhc22WKs7b4GRsC37NH8+JoN8iMOdAKEIUYFOvsvkMNTbpqMf2VUd29Iozuy1x/maWkUUyY+OgXFL9fVVZgEnoOkZvHH3+r8H3G3b820EzdXcS31kcwcqa1AA+o02h/pN2pjKzbuhGEqqswKVLmeKw5AIqC8pl+vhh/l/NLL4Gf4IqW40QT/eQe1ncnRVWW3M+XMuWVz4GMsJF3moqLaw0kTpAsH4bdzKFxKEa3qB/jqEe+5iGX7DhZ4c5IMktdkl2KRtLCa24GTuq/5NSeugCpXVSh6OIkvRXWbeYCJxDAWR4QMG/wDQJqRDzupQ0OuvbOqWmtrvpWjlLnbBdHU+kwmmCsBpjU8CY3VuZVGSgPwU/wI+Sf2WnD24eVtplIDtReqbyWUxEXAUbKaotfbQgVWeMdy1fzQa6BZ/vxQIJoVm2gCVk8FeoCzb9MUavAKWuOR8v32lFVOkJEArtfAJ5OZSfD0GXjAaMt54s7yHNF9PCBt81w=" - } - ], - "role": "model" - }, - "finishReason": "STOP", - "index": 0, - "finishMessage": "Model generated function call(s)." - } - ], - "usageMetadata": { - "promptTokenCount": 4568, - "candidatesTokenCount": 188, - "totalTokenCount": 5495, - "promptTokensDetails": [ - { - "modality": "TEXT", - "tokenCount": 4568 - } - ], - "thoughtsTokenCount": 739 - }, - "modelVersion": "gemini-2.5-pro", - "responseId": "64v5aLfxApaGqtsPr-2lgQc" -} diff --git a/examples/custom_backend/lib/backend.dart b/examples/custom_backend/lib/backend.dart deleted file mode 100644 index 8a670981d..000000000 --- a/examples/custom_backend/lib/backend.dart +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:genui/genui.dart'; - -import 'debug_utils.dart'; -import 'gemini_client.dart'; - -class UiSchemaDefinition { - final String prompt; - final List tools; - - const UiSchemaDefinition({required this.prompt, required this.tools}); - - factory UiSchemaDefinition.fromJson(Map json) => - UiSchemaDefinition( - prompt: json['prompt'] as String, - tools: (json['tools'] as List) - .map( - (x) => - GenUiFunctionDeclaration.fromJson(x as Map), - ) - .toList(), - ); - - Map toJson() => { - 'type': 'UiSchemaDefinition', - 'prompt': prompt, - 'tools': List.from(tools.map((x) => x.toJson())), - }; -} - -class Backend { - Backend(this.schema); - - final UiSchemaDefinition schema; - - Future sendRequest( - String request, { - required String? savedResponse, - }) async { - final ToolCall? toolCall = await GeminiClient.sendRequest( - tools: schema.tools, - request: '${schema.prompt}\n\nUser request:\n$request', - savedResponse: savedResponse, - ); - - if (toolCall == null) return null; - - if (!schema.tools.map((e) => e.name).contains(toolCall.name)) { - throw Exception( - 'Received unknown tool call: ${toolCall.name}. ' - 'Expected one of: ${schema.tools.map((e) => e.name).toList()}', - ); - } - - debugSaveToFileObject('toolCall', toolCall); - - return parseToolCall(toolCall, toolCall.name); - } -} diff --git a/examples/custom_backend/lib/debug_utils.dart b/examples/custom_backend/lib/debug_utils.dart deleted file mode 100644 index 879604a63..000000000 --- a/examples/custom_backend/lib/debug_utils.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; -import 'package:convert/convert.dart'; - -int _i = 100; - -void debugSaveToFile(String name, String content, {String extension = 'txt'}) { - final dirName = - 'debug/${FixedDateTimeFormatter('YYYY-MM-DD_hh_mm_ss').encode(DateTime.now())}'; - final directory = Directory(dirName); - if (!directory.existsSync()) { - directory.createSync(recursive: true); - } - final file = File('$dirName/${_i++}-$name.log.$extension'); - file.writeAsStringSync(content); - print('Debug: ${Directory.current.path}/${file.path}'); -} - -void debugSaveToFileObject(String name, Object? content) { - final encoder = const JsonEncoder.withIndent(' '); - final String prettyJson = encoder.convert(content); - debugSaveToFile(name, prettyJson, extension: 'json'); -} diff --git a/examples/custom_backend/lib/gemini_client.dart b/examples/custom_backend/lib/gemini_client.dart deleted file mode 100644 index 18459fe94..000000000 --- a/examples/custom_backend/lib/gemini_client.dart +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: avoid_dynamic_calls - -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter/services.dart'; -import 'package:genui/genui.dart'; -import 'package:http/http.dart' as http; - -import 'debug_utils.dart'; - -// mode='ANY': -// The model is constrained to always predict a function call and -// guarantees function schema adherence. -// https://ai.google.dev/gemini-api/docs/function-calling?example=meeting#rest_2 - -abstract class GeminiClient { - static Future sendRequest({ - required List tools, - required String request, - required String? savedResponse, - }) async { - late final String? rawResponse; - if (savedResponse == null) { - rawResponse = await _getRawResponseFromApi(tools, request); - } else { - rawResponse = await _getSavedRawResponse(savedResponse); - } - - if (rawResponse == null) { - return null; - } - - final response = jsonDecode(rawResponse) as JsonMap; - - Map extractToolCallPart(JsonMap response) { - final candidates = response['candidates'] as List; - final firstCandidate = candidates.first as Map; - final content = firstCandidate['content'] as Map; - final parts = content['parts'] as List; - return parts.first as Map; - } - - debugSaveToFileObject('full-response', response); - final Map toolCallPart = extractToolCallPart(response); - final Object? functionCall = toolCallPart['functionCall']; - if (functionCall == null) return null; - return ToolCall.fromJson(functionCall as JsonMap); - } - - static Future _getSavedRawResponse(String savedResponse) async => - await rootBundle.loadString(savedResponse); - - static Future _getRawResponseFromApi( - List tools, - String request, - ) async { - debugSaveToFileObject('tools', tools); - - final String? apiKey = Platform.environment['GEMINI_API_KEY']; - if (apiKey == null) { - throw Exception('GEMINI_API_KEY environment variable not set.'); - } - - final Uri url = Uri.parse( - 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=$apiKey', - ); - - final String body = jsonEncode({ - 'contents': [ - { - 'role': 'user', - 'parts': [ - {'text': request}, - ], - }, - ], - 'tools': [ - {'function_declarations': tools.map((e) => e.toJson()).toList()}, - ], - 'tool_config': { - 'function_calling_config': { - 'mode': 'ANY', - 'allowed_function_names': tools.map((e) => e.name).toList(), - }, - }, - }); - - final http.Response response = await http.post( - url, - headers: {'Content-Type': 'application/json'}, - body: body, - ); - - if (response.statusCode == 200) { - debugSaveToFileObject('response-body', response.body); - return response.body; - } else { - throw Exception('Failed to send request: ${response.body}'); - } - } -} diff --git a/examples/custom_backend/lib/main.dart b/examples/custom_backend/lib/main.dart deleted file mode 100644 index ebab129ef..000000000 --- a/examples/custom_backend/lib/main.dart +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; - -import 'backend.dart'; - -void main() { - runApp(const MyApp()); -} - -const _title = 'Custom Backend Demo'; - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: _title, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key}); - - @override - State createState() => _MyHomePageState(); -} - -const requestText = 'Show me options how you can help me, using radio buttons.'; - -class _MyHomePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text(_title), - ), - body: Container( - padding: const EdgeInsets.all(16.0), - child: const _IntegrationTester(), - ), - ); - } -} - -class _IntegrationTester extends StatefulWidget { - const _IntegrationTester(); - - @override - State<_IntegrationTester> createState() => _IntegrationTesterState(); -} - -final Catalog _catalog = CoreCatalogItems.asCatalog(); -const _toolName = 'uiGenerationTool'; -final uiSchema = UiSchemaDefinition( - prompt: genUiTechPrompt([_toolName]), - tools: [ - catalogToFunctionDeclaration( - _catalog, - _toolName, - 'Generates Flutter UI based on user requests.', - ), - ], -); - -class _IntegrationTesterState extends State<_IntegrationTester> { - final _controller = TextEditingController(text: requestText); - - final _protocol = Backend(uiSchema); - late final A2uiMessageProcessor _a2uiMessageProcessor = A2uiMessageProcessor( - catalogs: [_catalog], - ); - String? _selectedResponse; - bool _isLoading = false; - String? _errorMessage; - String? _surfaceId; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - TextField(controller: _controller), - const SizedBox(height: 20.0), - _ResponseSelector((selected) => _selectedResponse = selected), - const SizedBox(height: 20.0), - IconButton( - onPressed: () async { - setState(() { - _isLoading = true; - _errorMessage = null; - }); - try { - print( - 'Sending request for _selectedResponse = ' - '$_selectedResponse ...', - ); - final ParsedToolCall? parsedToolCall = await _protocol - .sendRequest( - _controller.text, - savedResponse: _selectedResponse, - ); - if (parsedToolCall == null) { - print('No UI received.'); - setState(() { - _isLoading = false; - }); - return; - } - _surfaceId = parsedToolCall.surfaceId; - for (final A2uiMessage message in parsedToolCall.messages) { - _a2uiMessageProcessor.handleMessage(message); - } - print('UI received for surfaceId=${parsedToolCall.surfaceId}'); - setState(() => _isLoading = false); - } catch (e, callStack) { - print('Error connecting to backend: $e\n$callStack'); - setState(() { - _isLoading = false; - _errorMessage = e.toString(); - }); - } - }, - icon: const Icon(Icons.send), - ), - const SizedBox(height: 20.0), - Card( - elevation: 2.0, - child: Container( - height: 350, - width: 350, - alignment: Alignment.center, - child: _buildGeneratedUi(), - ), - ), - ], - ); - } - - Widget _buildGeneratedUi() { - if (_isLoading) { - return const CircularProgressIndicator(); - } - if (_errorMessage != null) { - return Text('$_errorMessage'); - } - final String? surfaceId = _surfaceId; - if (surfaceId == null) { - return const Text('_surfaceId == null'); - } - return GenUiSurface( - surfaceId: surfaceId, - host: _a2uiMessageProcessor, - defaultBuilder: (_) => const Text('Fallback to defaultBuilder'), - ); - } -} - -class _ResponseSelector extends StatefulWidget { - _ResponseSelector(this.onChanged); - - final ValueChanged onChanged; - - @override - State<_ResponseSelector> createState() => _ResponseSelectorState(); -} - -class _ResponseSelectorState extends State<_ResponseSelector> { - String? _selection; - - @override - Widget build(BuildContext context) { - return DropdownButton( - value: _selection, - - onChanged: (String? newValue) => setState(() { - _selection = newValue; - widget.onChanged(newValue); - }), - - items: savedResponseAssets.map((String? location) { - return DropdownMenuItem( - value: location, - child: Text(location ?? 'Request Gemini'), - ); - }).toList(), - ); - } -} - -const _numberOfSavedResponses = 3; -final Iterable savedResponseAssets = List.generate( - _numberOfSavedResponses + 1, - (index) => index == 0 ? null : 'assets/data/saved-response-$index.json', -); diff --git a/examples/custom_backend/macos/.gitignore b/examples/custom_backend/macos/.gitignore deleted file mode 100644 index 746adbb6b..000000000 --- a/examples/custom_backend/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/examples/custom_backend/macos/Flutter/Flutter-Debug.xcconfig b/examples/custom_backend/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/examples/custom_backend/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/custom_backend/macos/Flutter/Flutter-Release.xcconfig b/examples/custom_backend/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/examples/custom_backend/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/custom_backend/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/custom_backend/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817a5..000000000 --- a/examples/custom_backend/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/examples/custom_backend/macos/Runner.xcodeproj/project.pbxproj b/examples/custom_backend/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 285e05a12..000000000 --- a/examples/custom_backend/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* custom_backend.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "custom_backend.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* custom_backend.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* custom_backend.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.customBackend.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/custom_backend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/custom_backend"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.customBackend.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/custom_backend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/custom_backend"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.customBackend.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/custom_backend.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/custom_backend"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/examples/custom_backend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/custom_backend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/examples/custom_backend/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/examples/custom_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/custom_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index ef1f87ebe..000000000 --- a/examples/custom_backend/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/custom_backend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/custom_backend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/examples/custom_backend/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/examples/custom_backend/macos/Runner/AppDelegate.swift b/examples/custom_backend/macos/Runner/AppDelegate.swift deleted file mode 100644 index 43bd41192..000000000 --- a/examples/custom_backend/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19..000000000 --- a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba5..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226d..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e0..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72c..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfd..000000000 Binary files a/examples/custom_backend/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/examples/custom_backend/macos/Runner/Base.lproj/MainMenu.xib b/examples/custom_backend/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 09010a1fb..000000000 --- a/examples/custom_backend/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/custom_backend/macos/Runner/Configs/AppInfo.xcconfig b/examples/custom_backend/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 3fa561ec3..000000000 --- a/examples/custom_backend/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = custom_backend - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.customBackend - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/examples/custom_backend/macos/Runner/Configs/Debug.xcconfig b/examples/custom_backend/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd946..000000000 --- a/examples/custom_backend/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/examples/custom_backend/macos/Runner/Configs/Release.xcconfig b/examples/custom_backend/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f4956..000000000 --- a/examples/custom_backend/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/examples/custom_backend/macos/Runner/Configs/Warnings.xcconfig b/examples/custom_backend/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf478..000000000 --- a/examples/custom_backend/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/examples/custom_backend/macos/Runner/DebugProfile.entitlements b/examples/custom_backend/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 78c36cf44..000000000 --- a/examples/custom_backend/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - - diff --git a/examples/custom_backend/macos/Runner/Info.plist b/examples/custom_backend/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a..000000000 --- a/examples/custom_backend/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/examples/custom_backend/macos/Runner/MainFlutterWindow.swift b/examples/custom_backend/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 79861d1c4..000000000 --- a/examples/custom_backend/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/examples/custom_backend/macos/Runner/Release.entitlements b/examples/custom_backend/macos/Runner/Release.entitlements deleted file mode 100644 index 08ba3a3fa..000000000 --- a/examples/custom_backend/macos/Runner/Release.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/examples/custom_backend/macos/RunnerTests/RunnerTests.swift b/examples/custom_backend/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 8b03e329d..000000000 --- a/examples/custom_backend/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/examples/custom_backend/pubspec.yaml b/examples/custom_backend/pubspec.yaml deleted file mode 100644 index 086334174..000000000 --- a/examples/custom_backend/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: custom_backend -publish_to: "none" -version: 1.0.0+1 - -environment: - sdk: ">=3.9.2 <4.0.0" - flutter: ">=3.35.7 <4.0.0" - -resolution: workspace - -dependencies: - convert: ^3.1.2 - flutter: - sdk: flutter - genui: ^0.7.0 - http: ^1.6.0 - -dev_dependencies: - build_runner: ^2.7.1 - flutter_test: - sdk: flutter - json_schema_builder: ^0.1.3 - mockito: ^5.5.0 - -flutter: - uses-material-design: true - assets: - - assets/data/ diff --git a/examples/custom_backend/test/backend_api_test.dart b/examples/custom_backend/test/backend_api_test.dart deleted file mode 100644 index bee3e8989..000000000 --- a/examples/custom_backend/test/backend_api_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:custom_backend/backend.dart'; -import 'package:custom_backend/main.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; - -void main() { - setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); - }); - - for (final String? savedResponse in savedResponseAssets) { - // TODO: fix Gemini API keys to get live test working. - if (savedResponse == null) { - continue; - } - - // To update the saved responses, run the app, select "Request Gemini", - // and copy the console output of the "full-response" to the - // corresponding `saved-response-X.json` file in `assets/data/`. - test( - 'sendRequest works for $savedResponse', - () async { - final protocol = Backend(uiSchema); - final ParsedToolCall? result = await protocol.sendRequest( - requestText, - savedResponse: savedResponse, - ); - expect(result, isNotNull); - expect(result!.messages.length, 2); - expect(result.messages[0], isA()); - expect(result.messages[1], isA()); - }, - retry: 3, - timeout: const Timeout(Duration(minutes: 2)), - ); - } -} diff --git a/examples/simple_chat/README.md b/examples/simple_chat/README.md index 24268135d..b9e7d25d7 100644 --- a/examples/simple_chat/README.md +++ b/examples/simple_chat/README.md @@ -5,46 +5,33 @@ This application is a minimal example of how to use the `genui` package to creat ## Purpose The main goal of this example is to demonstrate the fundamental concepts of `genui` in a straightforward chat context. It shows how to: - -1. Initialize and use the `GenUiConversation`, the primary facade for the package. +1. Initialize and use the `SurfaceController`, the core engine for the package. 2. Provide a simple system prompt to guide the AI's behavior. 3. Send user messages to the AI and receive responses. 4. Handle the creation of new UI "surfaces" generated by the AI. 5. Render these dynamic UI surfaces within a standard chat message list. 6. Manage a conversation history that interleaves user text messages with AI-generated UI responses. -7. Support multiple AI backends (Firebase AI or Google Generative AI). Unlike more complex examples, this app does not define a custom widget catalog. Instead, it relies on the default `coreCatalog` provided by `genui`, meaning the AI can only respond with basic widgets like `Text`, `Column`, `ElevatedButton`, etc. ## How it Works -The application's logic is contained almost entirely within `lib/main.dart`. +The application's logic is contained almost entirely within `lib/chat_session.dart`. -1. **Initialization**: A `GenUiConversation` is created with a simple system prompt instructing it to act as a helpful assistant. It's configured with an `onSurfaceAdded` callback. +1. **Initialization**: A `SurfaceController` is created to manage the state of UI surfaces. 2. **User Input**: The user types a message into a `TextField` and hits send. 3. **Sending the Message**: - - The user's text is immediately added to the local message list and displayed on screen as a simple text message (e.g., "You: Hello"). - - The `genUiConversation.sendRequest()` method is called with the user's text wrapped in a `UserMessage`. + - The user's text is immediately added to the local message list. + - The request is sent to the `AiClient`. 4. **AI Response**: - - The `GenUiConversation` sends the conversation history to the configured `ContentGenerator` (either `FirebaseAiContentGenerator` or `GoogleGenerativeAiContentGenerator`). - - The AI model processes the prompt and generates `A2uiMessage`s (like `SurfaceUpdate`, `BeginRendering`). - - These messages are emitted on the `ContentGenerator.a2uiMessageStream`. + - The `AiClient` streams `A2uiMessage`s back. + - These messages are piped into the `SurfaceController`. 5. **UI Rendering**: - - `GenUiConversation` listens to the stream and processes the `A2uiMessage`s, invoking the appropriate callbacks like `onSurfaceAdded`. - - The `_handleSurfaceAdded` callback adds a new message item to the list, containing the `surfaceId`. - - The `ListView` rebuilds, and a `GenUiSurface` widget is rendered for the AI's message, dynamically building the UI based on the `UiDefinition` managed by `A2uiMessageProcessor`. + - The UI listens to `SurfaceController.surfaceUpdates` or `A2uiTransportAdapter` streams. + - When a surface is added, a `Surface` widget is rendered, dynamically building the UI based on the `UiDefinition` managed by `SurfaceController`. ## Getting Started -This example supports two AI backends: **Google Generative AI** (default) and **Firebase AI**. Choose the option that best fits your needs. - -### Option 1: Using Google Generative AI (Default) - -> **⚠️ Warning**: This option is for demo purposes only. For -> production applications, use Firebase AI (Option 2) which provides -> better security, quota management, and production-ready -> infrastructure. - 1. **Get an API Key**: Obtain a Google Cloud API key with access to the Generative Language API from the [Google AI Studio](https://aistudio.google.com/app/apikey). @@ -62,30 +49,3 @@ This example supports two AI backends: **Google Generative AI** (default) and ** export GEMINI_API_KEY=your_api_key_here flutter run -d --dart-define=GEMINI_API_KEY=$GEMINI_API_KEY ``` - -### Option 2: Using Firebase AI - -1. **Configure Firebase**: Follow the instructions in the main `genui` package [README.md](../../packages/genui/README.md#configure-firebase-ai-logic) to add Firebase to your Flutter app. You will need to: - - Set up a Firebase project - - Generate a `firebase_options.dart` file using the FlutterFire CLI. You can run `sh tool/refresh_firebase.sh ` from the repo root to help you set this up. - -2. **Switch the Backend**: In `lib/configuration.dart`, change the - `aiBackend` constant to use Firebase: - - ```dart - const AiBackend aiBackend = AiBackend.firebase; - ``` - -3. **Run the App**: - - ```bash - flutter run -d - ``` - -### Switching Between Backends - -To switch backends, modify the `aiBackend` constant in -`lib/configuration.dart`: -- `AiBackend.googleGenerativeAi` (default) - Uses Google Generative AI with - an API key -- `AiBackend.firebase` - Uses Firebase AI diff --git a/examples/simple_chat/android/gradle/wrapper/gradle-wrapper.jar b/examples/simple_chat/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100755 index 13372aef5..000000000 Binary files a/examples/simple_chat/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/examples/simple_chat/integration_test/app_test.dart b/examples/simple_chat/integration_test/app_test.dart new file mode 100644 index 000000000..1227653b6 --- /dev/null +++ b/examples/simple_chat/integration_test/app_test.dart @@ -0,0 +1,143 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:logging/logging.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:simple_chat/main.dart'; + +// Import from ../test via relative path since it is not in lib +import '../test/fake_ai_client.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Configure logging + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + group('Simple Chat Integration Tests', () { + testWidgets('render hello world sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_1_hello.json', + (tester, client) async { + expect(find.textContaining('Hello, World!'), findsOneWidget); + }, + ); + }); + }); + + testWidgets('render button sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_2_button.json', + (tester, client) async { + // Button might be ElevatedButton, TextButton, or FilledButton. + // Just finding text is safer for integration test unless we care + // about specific styling. + expect(find.text('Click Me'), findsOneWidget); + + // Interaction Verification + await tester.tap(find.text('Click Me')); + await tester.pump(); + // Button action does not trigger a response if the fake client is + // empty, but it should send the prompt. + expect( + client.receivedPrompts, + contains(contains('Button Clicked')), + ); + }, + ); + }); + }); + + testWidgets('render image sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_3_image.json', + (tester, client) async { + // Image widget should exist even if mocked. + expect(find.byType(Image), findsOneWidget); + }, + ); + }); + }); + + testWidgets('render form sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_4_form.json', + (tester, client) async { + // Debug dump if fails + expect(find.text('Type'), findsOneWidget); + expect(find.text('Size'), findsOneWidget); + expect(find.text('Submit Filters'), findsOneWidget); + }, + ); + }); + }); + + testWidgets('render mixed sample', (tester) async { + await mockNetworkImagesFor(() async { + await _runTestForSample( + tester, + 'integration_test/samples/sample_5_mixed.json', + (tester, client) async { + expect(find.text('Do you want to proceed?'), findsOneWidget); + expect(find.text('Yes, proceed'), findsOneWidget); + }, + ); + }); + }); + }); +} + +Future _runTestForSample( + WidgetTester tester, + String samplePath, + Future Function(WidgetTester, FakeAiClient) verify, +) async { + // Read sample file + final file = File(samplePath); + if (!file.existsSync()) { + fail('Sample file not found: $samplePath'); + } + final String jsonString = await file.readAsString(); + + // Initialize FakeAiClient + final fakeAiClient = FakeAiClient(); + + // Queue the response + // SurfaceController expects A2UI messages to be wrapped in markdown code + // blocks or detectable as structured content. Standard LLM behavior using + // GenUi is to return ```json ... ``` blocks. + fakeAiClient.addResponse('Here is the UI:\n```json\n$jsonString\n```'); + + await tester.pumpWidget( + MaterialApp(home: ChatScreen(aiClient: fakeAiClient)), + ); + + // Trigger a message to start the flow + await tester.enterText(find.byType(TextField), 'Test Trigger'); + await tester.tap(find.byIcon(Icons.send)); + await tester.pump(); // Start processing + + // Wait for response and rendering + // The FakeAiClient splits it into chunks with delays. + await tester.pumpAndSettle(); + + // Run verification + await verify(tester, fakeAiClient); +} diff --git a/examples/simple_chat/integration_test/samples/sample_1_hello.json b/examples/simple_chat/integration_test/samples/sample_1_hello.json new file mode 100644 index 000000000..a51e85b27 --- /dev/null +++ b/examples/simple_chat/integration_test/samples/sample_1_hello.json @@ -0,0 +1,22 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_1_hello", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_1_hello", + "components": [ + { + "id": "root", + "component": "Text", + "text": "Hello, World!" + } + ] + } + } +] diff --git a/examples/simple_chat/integration_test/samples/sample_2_button.json b/examples/simple_chat/integration_test/samples/sample_2_button.json new file mode 100644 index 000000000..b224f4fc6 --- /dev/null +++ b/examples/simple_chat/integration_test/samples/sample_2_button.json @@ -0,0 +1,40 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_2_button", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_2_button", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["the_button"] + }, + { + "id": "the_button", + "component": "Button", + "child": "btn_label", + "action": { + "event": { + "name": "submit", + "context": { + "value": "Button Clicked" + } + } + } + }, + { + "id": "btn_label", + "component": "Text", + "text": "Click Me" + } + ] + } + } +] diff --git a/examples/simple_chat/integration_test/samples/sample_3_image.json b/examples/simple_chat/integration_test/samples/sample_3_image.json new file mode 100644 index 000000000..89f447f82 --- /dev/null +++ b/examples/simple_chat/integration_test/samples/sample_3_image.json @@ -0,0 +1,23 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_3_image", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_3_image", + "components": [ + { + "id": "root", + "component": "Image", + "url": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png", + "usageHint": "mediumFeature" + } + ] + } + } +] diff --git a/examples/simple_chat/integration_test/samples/sample_4_form.json b/examples/simple_chat/integration_test/samples/sample_4_form.json new file mode 100644 index 000000000..57cb75c66 --- /dev/null +++ b/examples/simple_chat/integration_test/samples/sample_4_form.json @@ -0,0 +1,49 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_4_form", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_4_form", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["field_type", "field_size", "submit_btn"] + }, + { + "id": "field_type", + "component": "TextField", + "label": "Type", + "text": "" + }, + { + "id": "field_size", + "component": "TextField", + "label": "Size", + "text": "" + }, + { + "id": "submit_btn", + "component": "Button", + "child": "btn_text", + "action": { + "event": { + "name": "submit_form" + } + } + }, + { + "id": "btn_text", + "component": "Text", + "text": "Submit Filters" + } + ] + } + } +] diff --git a/examples/simple_chat/integration_test/samples/sample_5_mixed.json b/examples/simple_chat/integration_test/samples/sample_5_mixed.json new file mode 100644 index 000000000..2372fbf7a --- /dev/null +++ b/examples/simple_chat/integration_test/samples/sample_5_mixed.json @@ -0,0 +1,54 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "sample_5_mixed", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json" + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "sample_5_mixed", + "components": [ + { + "id": "root", + "component": "Column", + "children": ["question", "btn_yes", "btn_no"] + }, + { + "id": "question", + "component": "Text", + "text": "Do you want to proceed?" + }, + { + "id": "btn_yes", + "component": "Button", + "child": "yes_text", + "action": { + "event": { "name": "proceed_yes" } + } + }, + { + "id": "yes_text", + "component": "Text", + "text": "Yes, proceed" + }, + { + "id": "btn_no", + "component": "Button", + "child": "no_text", + "variant": "borderless", + "action": { + "event": { "name": "proceed_no" } + } + }, + { + "id": "no_text", + "component": "Text", + "text": "No, cancel" + } + ] + } + } +] diff --git a/examples/simple_chat/lib/ai_client.dart b/examples/simple_chat/lib/ai_client.dart new file mode 100644 index 000000000..12c5b9f62 --- /dev/null +++ b/examples/simple_chat/lib/ai_client.dart @@ -0,0 +1,63 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; + +import 'api_key/io_get_api_key.dart' + if (dart.library.html) 'api_key/web_get_api_key.dart'; + +/// An abstract interface for AI clients. +abstract interface class AiClient { + /// Sends a message stream request to the AI service. + /// + /// [prompt] is the user's message. + /// [history] is the conversation history. + Stream sendStream( + String prompt, { + required List history, + }); + + /// Dispose of resources. + void dispose(); +} + +/// An implementation of [AiClient] using `package:dartantic_ai`. +class DartanticAiClient implements AiClient { + DartanticAiClient({String? modelName}) { + final String apiKey = getApiKey(); + _provider = dartantic.GoogleProvider(apiKey: apiKey); + _agent = dartantic.Agent.forProvider( + _provider, + chatModelName: modelName ?? 'gemini-3-flash-preview', + ); + } + + late final dartantic.GoogleProvider _provider; + late final dartantic.Agent _agent; + + @override + Stream sendStream( + String prompt, { + required List history, + }) async* { + final Stream> stream = _agent.sendStream( + prompt, + history: history, + ); + + await for (final result in stream) { + if (result.output.isNotEmpty) { + yield result.output; + } + } + } + + @override + void dispose() { + // Dartantic Agent/Provider doesn't strictly require disposal currently, + // but good to have the hook. + } +} diff --git a/examples/simple_chat/lib/chat_session.dart b/examples/simple_chat/lib/chat_session.dart new file mode 100644 index 000000000..5fbce193f --- /dev/null +++ b/examples/simple_chat/lib/chat_session.dart @@ -0,0 +1,162 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; +import 'package:flutter/foundation.dart'; +import 'package:genui/genui.dart'; + +import 'ai_client.dart'; +import 'message.dart'; + +/// A class that manages the chat session state and logic. +class ChatSession extends ChangeNotifier { + ChatSession({required AiClient aiClient}) : _aiClient = aiClient { + _init(); + } + + final AiClient _aiClient; + + final List _messages = []; + List get messages => List.unmodifiable(_messages); + + late final SurfaceController _surfaceController; + SurfaceHost get surfaceController => _surfaceController; + + late final A2uiTransportAdapter _transportAdapter; + A2uiTransportAdapter get transportAdapter => _transportAdapter; + + final List _chatHistory = []; + + bool _isProcessing = false; + bool get isProcessing => _isProcessing; + + void _init() { + final Catalog catalog = BasicCatalogItems.asCatalog(); + + // Initialize Message Processor + _surfaceController = SurfaceController(catalogs: [catalog]); + + // Initialize A2uiTransportAdapter + _transportAdapter = A2uiTransportAdapter(); + + // Wire controller to processor + _transportAdapter.incomingMessages.listen(_surfaceController.handleMessage); + + // Listen to UI state updates from the processor + _surfaceController.surfaceUpdates.listen((SurfaceUpdate update) { + if (update is SurfaceAdded) { + // Check if we already have a message with this surfaceId + final bool exists = _messages.any( + (m) => m.surfaceId == update.surfaceId, + ); + + if (!exists) { + _messages.add( + Message(isUser: false, text: null, surfaceId: update.surfaceId), + ); + notifyListeners(); + } + } + }); + + // Listen to client events (interactions) from the UI + _surfaceController.onSubmit.listen(_handleChatMessage); + + final promptBuilder = PromptBuilder.chat( + catalog: catalog, + instructions: + 'You are a helpful assistant who chats with a user. ' + 'Your responses should contain acknowledgment of the user message.', + ); + + // Add system instruction to history + _chatHistory.add(dartantic.ChatMessage.system(promptBuilder.systemPrompt)); + } + + void _handleChatMessage(ChatMessage event) { + genUiLogger.info('Received chat message: ${event.toJson()}'); + final buffer = StringBuffer(); + for (final dartantic.StandardPart part in event.parts) { + if (part.isUiInteractionPart) { + buffer.write(part.asUiInteractionPart!.interaction); + } else if (part is TextPart) { + buffer.write(part.text); + } + } + final text = buffer.toString(); + if (text.isNotEmpty) { + _sendInteraction(text); + } + } + + Future _sendInteraction(String text) async { + _chatHistory.add(dartantic.ChatMessage.user(text)); + await _performGeneration(text); + } + + Future sendMessage(String text) async { + if (text.isEmpty) return; + + _messages.add(Message(isUser: true, text: 'You: $text')); + _chatHistory.add(dartantic.ChatMessage.user(text)); + + await _performGeneration(text); + } + + Future _performGeneration(String prompt) async { + _isProcessing = true; + notifyListeners(); + + try { + var fullResponseText = ''; + + // Create a message controller for the AI response + final aiMessageController = Message(isUser: false, text: 'AI: '); + _messages.add(aiMessageController); + notifyListeners(); + + // Listen for text updates from the controller to update the UI + final StreamSubscription subscription = _transportAdapter + .incomingText + .listen((chunk) { + aiMessageController.text = (aiMessageController.text ?? '') + chunk; + notifyListeners(); + }); + + // Use sendStream() to receive chunks of the response. + final Stream stream = _aiClient.sendStream( + prompt, + history: List.of(_chatHistory), + ); + + await for (final String chunk in stream) { + if (chunk.isNotEmpty) { + fullResponseText += chunk; + _transportAdapter.addChunk(chunk); + } + } + + await subscription.cancel(); + + _chatHistory.add(dartantic.ChatMessage.model(fullResponseText)); + } catch (exception, stackTrace) { + genUiLogger.severe('Error generating content', exception, stackTrace); + // We might want to expose errors via a listener or separate stream + // For now, let's just log it. In a real app, we'd handle error states. + } finally { + _isProcessing = false; + notifyListeners(); + } + } + + @override + void dispose() { + _surfaceController.dispose(); + _transportAdapter.dispose(); + _aiClient.dispose(); + super.dispose(); + } +} diff --git a/examples/simple_chat/lib/configuration.dart b/examples/simple_chat/lib/configuration.dart deleted file mode 100644 index 230831bb2..000000000 --- a/examples/simple_chat/lib/configuration.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Enum for selecting which AI backend to use. -enum AiBackend { - /// Use Firebase AI - firebase, - - /// Use Google Generative AI - googleGenerativeAi, -} - -/// Configuration for which AI backend to use. -/// Change this value to switch between backends. -const AiBackend aiBackend = AiBackend.googleGenerativeAi; diff --git a/examples/simple_chat/lib/main.dart b/examples/simple_chat/lib/main.dart index b6d495589..131164360 100644 --- a/examples/simple_chat/lib/main.dart +++ b/examples/simple_chat/lib/main.dart @@ -2,46 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart'; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; import 'package:logging/logging.dart'; -// If you want to convert to using Firebase AI, run: -// -// sh tool/refresh_firebase.sh -// -// to refresh the Firebase configuration for a specific Firebase project. -// and uncomment the Firebase initialization code and import below that is -// marked with UNCOMMENT_FOR_FIREBASE, and set the value of `aiBackend` to -// `AiBackend.firebase` in `lib/configuration.dart`. - -// import 'firebase_options.dart'; // UNCOMMENT_FOR_FIREBASE - -// Conditionally import non-web version so we can read from shell env vars in -// non-web version. -import 'api_key/io_get_api_key.dart' - if (dart.library.html) 'api_key/web_get_api_key.dart'; -import 'configuration.dart'; +import 'ai_client.dart'; +import 'chat_session.dart'; import 'message.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - - // Only initialize Firebase if we are using the Firebase backend. - if (aiBackend == AiBackend.firebase) { - await Firebase.initializeApp( - // UNCOMMENT_FOR_FIREBASE (See top of file for details) - // options: DefaultFirebaseOptions.currentPlatform, - ); - } - - configureGenUiLogging(level: Level.ALL); - + // Configure logging for the app. + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); runApp(const MyApp()); } @@ -50,16 +24,22 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue); return MaterialApp( - title: 'Simple Chat', - theme: ThemeData(primarySwatch: Colors.blue), + title: 'Simple Chat Controller', + theme: ThemeData(colorScheme: colorScheme), + darkTheme: ThemeData( + colorScheme: colorScheme.copyWith(brightness: Brightness.dark), + ), home: const ChatScreen(), ); } } class ChatScreen extends StatefulWidget { - const ChatScreen({super.key}); + const ChatScreen({super.key, this.aiClient}); + + final AiClient? aiClient; @override State createState() => _ChatScreenState(); @@ -67,152 +47,91 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final TextEditingController _textController = TextEditingController(); - final List _messages = []; - late final GenUiConversation _genUiConversation; - late final A2uiMessageProcessor _a2uiMessageProcessor; final ScrollController _scrollController = ScrollController(); + late final ChatSession _chatSession; @override void initState() { super.initState(); - final Catalog catalog = CoreCatalogItems.asCatalog(); - _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [catalog]); - - final systemInstruction = - '''You are a helpful assistant who chats with a user, -giving exactly one response for each user message. -Your responses should contain acknowledgment -of the user message. - - -IMPORTANT: When you generate UI in a response, you MUST always create -a new surface with a unique `surfaceId`. Do NOT reuse or update -existing `surfaceId`s. Each UI response must be in its own new surface. - -${GenUiPromptFragments.basicChat}'''; - - // Create the appropriate content generator based on configuration - final ContentGenerator contentGenerator = switch (aiBackend) { - AiBackend.googleGenerativeAi => () { - return GoogleGenerativeAiContentGenerator( - catalog: catalog, - systemInstruction: systemInstruction, - apiKey: getApiKey(), - ); - }(), - AiBackend.firebase => FirebaseAiContentGenerator( - catalog: catalog, - systemInstruction: systemInstruction, - ), - }; - - _genUiConversation = GenUiConversation( - a2uiMessageProcessor: _a2uiMessageProcessor, - contentGenerator: contentGenerator, - onSurfaceAdded: _handleSurfaceAdded, - onTextResponse: _onTextResponse, - onError: (error) { - genUiLogger.severe( - 'Error from content generator', - error.error, - error.stackTrace, - ); - }, + _chatSession = ChatSession( + aiClient: widget.aiClient ?? DartanticAiClient(), ); - } - - void _handleSurfaceAdded(SurfaceAdded surface) { - if (!mounted) return; - setState(() { - _messages.add(MessageController(surfaceId: surface.surfaceId)); - }); - _scrollToBottom(); - } - - void _onTextResponse(String text) { - if (!mounted) return; - setState(() { - _messages.add(MessageController(text: 'AI: $text')); - }); - _scrollToBottom(); + // Add a listener to scroll to bottom when messages change. + _chatSession.addListener(_scrollToBottom); } @override Widget build(BuildContext context) { - final String title = switch (aiBackend) { - AiBackend.googleGenerativeAi => 'Chat with Google Generative AI', - AiBackend.firebase => 'Chat with Firebase AI', - }; - - return Scaffold( - appBar: AppBar(title: Text(title)), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView.builder( - controller: _scrollController, - itemCount: _messages.length, - itemBuilder: (context, index) { - final MessageController message = _messages[index]; - return ListTile( - title: MessageView(message, _genUiConversation.host), - ); - }, - ), - ), + return ListenableBuilder( + listenable: _chatSession, + builder: (context, _) { + return Scaffold( + appBar: AppBar(title: const Text('Chat (Controller + Dartantic)')), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: _chatSession.messages.length, + itemBuilder: (context, index) { + final Message message = _chatSession.messages[index]; + // Pass the controller as the host. + return ListTile( + title: MessageView( + message, + _chatSession.surfaceController, + ), + tileColor: message.isUser + ? Colors.blue.withValues(alpha: 0.1) + : null, + ); + }, + ), + ), - ValueListenableBuilder( - valueListenable: _genUiConversation.isProcessing, - builder: (_, isProcessing, _) { - if (!isProcessing) return Container(); - return const Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - ); - }, - ), + if (_chatSession.isProcessing) + const Padding( + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _textController, - decoration: const InputDecoration( - hintText: 'Type your message...', + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + decoration: const InputDecoration( + hintText: 'Type your message...', + ), + enabled: !_chatSession.isProcessing, + onSubmitted: (_) => _sendMessage(), + ), ), - onSubmitted: (_) => _sendMessage(), - ), - ), - IconButton( - icon: const Icon(Icons.send), - onPressed: _sendMessage, + IconButton( + icon: const Icon(Icons.send), + onPressed: _chatSession.isProcessing + ? null + : _sendMessage, + ), + ], ), - ], - ), + ), + ], ), - ], - ), - ), + ), + ); + }, ); } - void _sendMessage() { + Future _sendMessage() async { final String text = _textController.text; - if (text.isEmpty) { - return; - } + if (text.isEmpty) return; _textController.clear(); - - setState(() { - _messages.add(MessageController(text: 'You: $text')); - }); - - _scrollToBottom(); - - unawaited(_genUiConversation.sendRequest(UserMessage([TextPart(text)]))); + await _chatSession.sendMessage(text); } void _scrollToBottom() { @@ -229,7 +148,9 @@ ${GenUiPromptFragments.basicChat}'''; @override void dispose() { - _genUiConversation.dispose(); + _chatSession.dispose(); + _textController.dispose(); + _scrollController.dispose(); super.dispose(); } } diff --git a/examples/simple_chat/lib/message.dart b/examples/simple_chat/lib/message.dart index d72bc23e3..403b0965f 100644 --- a/examples/simple_chat/lib/message.dart +++ b/examples/simple_chat/lib/message.dart @@ -5,26 +5,27 @@ import 'package:flutter/material.dart'; import 'package:genui/genui.dart'; -class MessageController { - MessageController({this.text, this.surfaceId}) +class Message { + Message({this.text, this.surfaceId, this.isUser = false}) : assert((surfaceId == null) != (text == null)); - final String? text; + String? text; final String? surfaceId; + final bool isUser; } class MessageView extends StatelessWidget { - const MessageView(this.controller, this.host, {super.key}); + const MessageView(this.message, this.host, {super.key}); - final MessageController controller; - final GenUiHost host; + final Message message; + final SurfaceHost host; @override Widget build(BuildContext context) { - final String? surfaceId = controller.surfaceId; + final String? surfaceId = message.surfaceId; - if (surfaceId == null) return Text(controller.text ?? ''); + if (surfaceId == null) return Text(message.text ?? ''); - return GenUiSurface(host: host, surfaceId: surfaceId); + return Surface(surfaceContext: host.contextFor(surfaceId)); } } diff --git a/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc b/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc index e71a16d23..f6f23bfe9 100644 --- a/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc +++ b/examples/simple_chat/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/examples/simple_chat/linux/flutter/generated_plugins.cmake b/examples/simple_chat/linux/flutter/generated_plugins.cmake index 2e1de87a7..f16b4c342 100644 --- a/examples/simple_chat/linux/flutter/generated_plugins.cmake +++ b/examples/simple_chat/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift index c6c180db8..8236f5728 100644 --- a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,8 @@ import FlutterMacOS import Foundation -import firebase_app_check -import firebase_auth -import firebase_core +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/examples/simple_chat/macos/Podfile.lock b/examples/simple_chat/macos/Podfile.lock index 6a113d37b..ed05ac66c 100644 --- a/examples/simple_chat/macos/Podfile.lock +++ b/examples/simple_chat/macos/Podfile.lock @@ -1,130 +1,21 @@ PODS: - - AppCheckCore (11.2.0): - - GoogleUtilities/Environment (~> 8.0) - - GoogleUtilities/UserDefaults (~> 8.0) - - PromisesObjC (~> 2.4) - - Firebase/AppCheck (12.4.0): - - Firebase/CoreOnly - - FirebaseAppCheck (~> 12.4.0) - - Firebase/Auth (12.4.0): - - Firebase/CoreOnly - - FirebaseAuth (~> 12.4.0) - - Firebase/CoreOnly (12.4.0): - - FirebaseCore (~> 12.4.0) - - firebase_app_check (0.4.1-2): - - Firebase/AppCheck (~> 12.4.0) - - Firebase/CoreOnly (~> 12.4.0) - - firebase_core - - FlutterMacOS - - firebase_auth (6.1.2): - - Firebase/Auth (~> 12.4.0) - - Firebase/CoreOnly (~> 12.4.0) - - firebase_core - - FlutterMacOS - - firebase_core (4.2.1): - - Firebase/CoreOnly (~> 12.4.0) - - FlutterMacOS - - FirebaseAppCheck (12.4.0): - - AppCheckCore (~> 11.0) - - FirebaseAppCheckInterop (~> 12.4.0) - - FirebaseCore (~> 12.4.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/UserDefaults (~> 8.1) - - FirebaseAppCheckInterop (12.4.0) - - FirebaseAuth (12.4.0): - - FirebaseAppCheckInterop (~> 12.4.0) - - FirebaseAuthInterop (~> 12.4.0) - - FirebaseCore (~> 12.4.0) - - FirebaseCoreExtension (~> 12.4.0) - - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - - GoogleUtilities/Environment (~> 8.1) - - GTMSessionFetcher/Core (< 6.0, >= 3.4) - - RecaptchaInterop (~> 101.0) - - FirebaseAuthInterop (12.4.0) - - FirebaseCore (12.4.0): - - FirebaseCoreInternal (~> 12.4.0) - - GoogleUtilities/Environment (~> 8.1) - - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreExtension (12.4.0): - - FirebaseCore (~> 12.4.0) - - FirebaseCoreInternal (12.4.0): - - "GoogleUtilities/NSData+zlib (~> 8.1)" - FlutterMacOS (1.0.0) - - GoogleUtilities/AppDelegateSwizzler (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Logger - - GoogleUtilities/Network - - GoogleUtilities/Privacy - - GoogleUtilities/Environment (8.1.0): - - GoogleUtilities/Privacy - - GoogleUtilities/Logger (8.1.0): - - GoogleUtilities/Environment - - GoogleUtilities/Privacy - - GoogleUtilities/Network (8.1.0): - - GoogleUtilities/Logger - - "GoogleUtilities/NSData+zlib" - - GoogleUtilities/Privacy - - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (8.1.0)": - - GoogleUtilities/Privacy - - GoogleUtilities/Privacy (8.1.0) - - GoogleUtilities/Reachability (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GoogleUtilities/UserDefaults (8.1.0): - - GoogleUtilities/Logger - - GoogleUtilities/Privacy - - GTMSessionFetcher/Core (5.0.0) - - PromisesObjC (2.4.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS DEPENDENCIES: - - firebase_app_check (from `Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos`) - - firebase_auth (from `Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos`) - - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - -SPEC REPOS: - trunk: - - AppCheckCore - - Firebase - - FirebaseAppCheck - - FirebaseAppCheckInterop - - FirebaseAuth - - FirebaseAuthInterop - - FirebaseCore - - FirebaseCoreExtension - - FirebaseCoreInternal - - GoogleUtilities - - GTMSessionFetcher - - PromisesObjC + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: - firebase_app_check: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_app_check/macos - firebase_auth: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_auth/macos - firebase_core: - :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos FlutterMacOS: :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f - Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e - firebase_app_check: 55b42060763e07374ddff10821929c2174d7c851 - firebase_auth: 87ea88759d0282ccf1e2afe96f88ff3c07547938 - firebase_core: e054894ab56033ef9bcbe2d9eac9395e5306e2fc - FirebaseAppCheck: 73721d98fa29cf199da6004e57715cbaddd49651 - FirebaseAppCheckInterop: f734c802f21fe1da0837708f0f9a27218c8a4ed0 - FirebaseAuth: 4a2aed737c84114a9d9b33d11ae1b147d6b94889 - FirebaseAuthInterop: 858e6b754966e70740a4370dd1503dfffe6dbb49 - FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3 - FirebaseCoreExtension: 7e1f7118ee970e001a8013719fb90950ee5e0018 - FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - GTMSessionFetcher: 02d6e866e90bc236f48a703a041dfe43e6221a29 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/examples/simple_chat/pubspec.yaml b/examples/simple_chat/pubspec.yaml index 650a58176..521162698 100644 --- a/examples/simple_chat/pubspec.yaml +++ b/examples/simple_chat/pubspec.yaml @@ -13,17 +13,18 @@ environment: resolution: workspace dependencies: - firebase_core: ^4.2.1 + dartantic_ai: ^3.0.0 flutter: sdk: flutter genui: ^0.7.0 - genui_firebase_ai: ^0.7.0 - genui_google_generative_ai: ^0.7.0 logging: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + network_image_mock: ^2.1.1 flutter: uses-material-design: true diff --git a/examples/simple_chat/test/fake_ai_client.dart b/examples/simple_chat/test/fake_ai_client.dart new file mode 100644 index 000000000..06442605b --- /dev/null +++ b/examples/simple_chat/test/fake_ai_client.dart @@ -0,0 +1,66 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; +import 'package:genui/genui.dart'; +import 'package:simple_chat/ai_client.dart'; + +/// A fake implementation of [AiClient] for testing. +class FakeAiClient implements AiClient { + final StreamController _a2uiMessageController = + StreamController.broadcast(); + + final StreamController _textResponseController = + StreamController.broadcast(); + + // Queue of responses to send for each request. + final List _responses = []; + + final List _receivedPrompts = []; + List get receivedPrompts => List.unmodifiable(_receivedPrompts); + + Stream get a2uiMessageStream => _a2uiMessageController.stream; + + Stream get textResponseStream => _textResponseController.stream; + + /// Adds a response to the queue. + void addResponse(String response) { + _responses.add(response); + } + + @override + Stream sendStream( + String prompt, { + required List history, + }) async* { + _receivedPrompts.add(prompt); + if (_responses.isEmpty) { + yield 'I have no response for that.'; + return; + } + + final String response = _responses.removeAt(0); + + // Simulate streaming by yielding characters or chunks + // For simplicity, we can just yield the whole thing or split it. + // Let's split it into small chunks to simulate network. + const chunkSize = 10; + for (var i = 0; i < response.length; i += chunkSize) { + final int end = (i + chunkSize < response.length) + ? i + chunkSize + : response.length; + yield response.substring(i, end); + // tiny delay + await Future.delayed(const Duration(milliseconds: 10)); + } + } + + @override + void dispose() { + _a2uiMessageController.close(); + _textResponseController.close(); + } +} diff --git a/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc b/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc index d141b74f5..4f7884874 100644 --- a/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc +++ b/examples/simple_chat/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); - FirebaseCorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/examples/simple_chat/windows/flutter/generated_plugins.cmake b/examples/simple_chat/windows/flutter/generated_plugins.cmake index 29944d5b1..88b22e5c7 100644 --- a/examples/simple_chat/windows/flutter/generated_plugins.cmake +++ b/examples/simple_chat/windows/flutter/generated_plugins.cmake @@ -3,8 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST - firebase_auth - firebase_core + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/travel_app/.metadata b/examples/travel_app/.metadata index 5492cf186..3105b41cc 100644 --- a/examples/travel_app/.metadata +++ b/examples/travel_app/.metadata @@ -4,8 +4,8 @@ # This file should be version controlled and should not be manually edited. version: - revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" - channel: "stable" + revision: "61b241292e19923008624a8e25b9e16e0ba988f1" + channel: "main" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - platform: linux - create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + create_revision: 61b241292e19923008624a8e25b9e16e0ba988f1 + base_revision: 61b241292e19923008624a8e25b9e16e0ba988f1 + - platform: macos + create_revision: 61b241292e19923008624a8e25b9e16e0ba988f1 + base_revision: 61b241292e19923008624a8e25b9e16e0ba988f1 # User provided section diff --git a/examples/travel_app/README.md b/examples/travel_app/README.md index 31f114a4f..ea8eda594 100644 --- a/examples/travel_app/README.md +++ b/examples/travel_app/README.md @@ -23,7 +23,7 @@ This example highlights several core concepts of the `genui` package: - **Dynamic UI Generation**: The entire user interface is constructed on-the-fly by the AI based on the conversation. - **Component Catalog**: The AI builds the UI from a custom, domain-specific catalog of widgets defined in `lib/src/catalog.dart`. This includes widgets like `TravelCarousel`, `ItineraryEntry`, and `OptionsFilterChipInput`. - **System Prompt Engineering**: The behavior of the AI is guided by a detailed system prompt located in `lib/src/travel_planner_page.dart`. This prompt instructs the AI on how to act like a travel agent and which widgets to use in various scenarios. -- **Dynamic UI State Management**: The `GenUiConversation` and `A2uiMessageProcessor` from `genui` handle the orchestration of AI interaction, state of the dynamically generated UI surfaces, and event processing. +- **Dynamic UI State Management**: The `Conversation` and `SurfaceController` from `genui` handle the orchestration of AI interaction, state of the dynamically generated UI surfaces, and event processing. - **Multiple AI Backends**: The app supports switching between **Google Generative AI** (direct API) and **Firebase Vertex AI**. This is configured in `lib/src/config/configuration.dart`. - **Tool Use**: The AI uses tools like `ListHotelsTool` to fetch real-world data (mocked in this example) and present it to the user. - **Widget Catalog**: A dedicated tab allows developers to inspect all available widgets in the catalog, facilitating development and debugging. diff --git a/examples/travel_app/integration_test/app_test.dart b/examples/travel_app/integration_test/app_test.dart index 06a7cd2b6..432ec4bdd 100644 --- a/examples/travel_app/integration_test/app_test.dart +++ b/examples/travel_app/integration_test/app_test.dart @@ -5,26 +5,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; -import 'package:genui/test/fake_content_generator.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:logging/logging.dart'; import 'package:travel_app/main.dart' as app; +import 'package:travel_app/src/fake_ai_client.dart'; void main() { + configureLogging(level: Level.ALL); IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Initial UI test', () { testWidgets('send a request and verify the UI', (tester) async { - final mockContentGenerator = FakeContentGenerator(); - mockContentGenerator.addA2uiMessage(A2uiMessage.fromJson(_baliResponse)); - - runApp(app.TravelApp(contentGenerator: mockContentGenerator)); + final mockClient = FakeAiClient(); + runApp(app.TravelApp(aiClient: mockClient)); await tester.pumpAndSettle(); await tester.enterText(find.byType(EditableText), 'Plan a trip to Bali'); await tester.tap(find.byIcon(Icons.send)); + + mockClient.addA2uiMessage(A2uiMessage.fromJson(_baliCreateSurface)); + mockClient.addA2uiMessage(A2uiMessage.fromJson(_baliResponse)); + await tester.pumpAndSettle(); expect( - find.text( + find.textContaining( 'Great! I can help you plan a fantastic trip to Bali. To ' 'get started, what kind of experience are you looking for?', findRichText: true, @@ -40,153 +44,108 @@ void main() { }); } +const Map _baliCreateSurface = { + 'version': 'v0.9', + 'createSurface': { + 'surfaceId': 'bali_trip_planning_intro', + 'catalogId': 'https://a2ui.org/specification/v0_9/standard_catalog.json', + 'sendDataModel': true, + }, +}; + const Map _baliResponse = { - 'actions': [ - { - 'action': 'add', - 'surfaceId': 'bali_trip_planning_intro', - 'definition': { - 'root': 'main_column', - 'widgets': [ - { - 'id': 'main_column', - 'widget': { - 'Column': { - 'children': ['welcome_text', 'bali_carousel', 'trip_filters'], - 'spacing': 16, - 'crossAxisAlignment': 'start', - 'mainAxisAlignment': 'start', - }, - }, - }, - { - 'widget': { - 'Text': { - 'text': - 'Great! I can help you plan a fantastic trip to Bali. To ' - 'get started, what kind of experience are you looking for?', - }, - }, - 'id': 'welcome_text', - }, - { - 'id': 'bali_carousel', - 'widget': { - 'TravelCarousel': { - 'items': [ - { - 'imageChildId': 'bali_memorial_image', - 'title': 'Cultural Immersion', - }, - { - 'imageChildId': 'nyepi_festival_image', - 'title': 'Festivals and Traditions', - }, - { - 'title': 'Beach Relaxation', - 'imageChildId': 'kata_noi_beach_image', - }, - ], - }, - }, - }, - { - 'id': 'bali_memorial_image', - 'widget': { - 'Image': { - 'fit': 'cover', - 'location': 'assets/travel_images/bali_memorial.jpg', - }, - }, - }, - { - 'id': 'nyepi_festival_image', - 'widget': { - 'Image': { - 'fit': 'cover', - 'location': 'assets/travel_images/nyepi_festival_bali.jpg', - }, - }, - }, - { - 'id': 'kata_noi_beach_image', - 'widget': { - 'Image': { - 'fit': 'cover', - 'location': - 'assets/travel_images/kata_noi_beach_phuket_thailand.jpg', - }, - }, - }, - { - 'widget': { - 'FilterChipGroup': { - 'submitLabel': 'Plan My Trip', - 'children': [ - 'travel_style_chip', - 'budget_chip', - 'duration_chip', - ], - }, - }, - 'id': 'trip_filters', - }, - { - 'widget': { - 'OptionsFilterChip': { - 'iconChild': 'travel_icon_hiking', - 'options': [ - 'Relaxation', - 'Adventure', - 'Culture', - 'Family Fun', - 'Romantic Getaway', - ], - 'chipLabel': 'Travel Style', - }, - }, - 'id': 'travel_style_chip', - }, - { - 'widget': { - 'TravelIcon': {'icon': 'hiking'}, - }, - 'id': 'travel_icon_hiking', - }, - { - 'widget': { - 'OptionsFilterChip': { - 'options': ['Economy', 'Mid-range', 'Luxury'], - 'iconChild': 'travel_icon_wallet', - 'chipLabel': 'Budget', - }, - }, - 'id': 'budget_chip', - }, + 'version': 'v0.9', + 'updateComponents': { + 'surfaceId': 'bali_trip_planning_intro', + 'components': [ + { + 'id': 'root', + 'component': 'Column', + 'children': ['welcome_text', 'bali_carousel', 'trip_filters'], + 'spacing': 16, + 'crossAxisAlignment': 'start', + 'mainAxisAlignment': 'start', + }, + { + 'id': 'welcome_text', + 'component': 'Text', + 'text': + 'Great! I can help you plan a fantastic trip to Bali. To ' + 'get started, what kind of experience are you looking for?', + }, + { + 'id': 'bali_carousel', + 'component': 'TravelCarousel', + 'items': [ { - 'id': 'travel_icon_wallet', - 'widget': { - 'TravelIcon': {'icon': 'wallet'}, - }, + 'imageChildId': 'bali_memorial_image', + 'description': 'Cultural Immersion', + 'action': {'name': 'selectExperience'}, }, { - 'id': 'duration_chip', - 'widget': { - 'OptionsFilterChip': { - 'chipLabel': 'Duration', - 'options': ['3-5 Days', '1 Week', '10+ Days'], - 'iconChild': 'travel_icon_calendar', - }, - }, + 'imageChildId': 'nyepi_festival_image', + 'description': 'Festivals and Traditions', + 'action': {'name': 'selectExperience'}, }, { - 'widget': { - 'TravelIcon': {'icon': 'calendar'}, - }, - 'id': 'travel_icon_calendar', + 'imageChildId': 'kata_noi_beach_image', + 'description': 'Beach Relaxation', + 'action': {'name': 'selectExperience'}, }, ], }, - }, - ], + { + 'id': 'bali_memorial_image', + 'component': 'Image', + 'fit': 'cover', + 'url': 'assets/travel_images/bali_memorial.jpg', + }, + { + 'id': 'nyepi_festival_image', + 'component': 'Image', + 'fit': 'cover', + 'url': 'assets/travel_images/nyepi_festival_bali.jpg', + }, + { + 'id': 'kata_noi_beach_image', + 'component': 'Image', + 'fit': 'cover', + 'url': 'assets/travel_images/kata_noi_beach_phuket_thailand.jpg', + }, + { + 'id': 'trip_filters', + 'component': 'InputGroup', + 'submitLabel': 'Plan My Trip', + 'children': ['travel_style_chip', 'budget_chip', 'duration_chip'], + 'action': {'name': 'plan_trip'}, + }, + { + 'id': 'travel_style_chip', + 'component': 'OptionsFilterChipInput', + 'iconName': 'location', + 'options': [ + 'Relaxation', + 'Adventure', + 'Culture', + 'Family Fun', + 'Romantic Getaway', + ], + 'chipLabel': 'Travel Style', + }, + { + 'id': 'budget_chip', + 'component': 'OptionsFilterChipInput', + 'options': ['Economy', 'Mid-range', 'Luxury'], + 'iconName': 'wallet', + 'chipLabel': 'Budget', + }, + { + 'id': 'duration_chip', + 'component': 'OptionsFilterChipInput', + 'chipLabel': 'Duration', + 'options': ['3-5 Days', '1 Week', '10+ Days'], + 'iconName': 'calendar', + }, + ], + }, }; diff --git a/examples/travel_app/lib/main.dart b/examples/travel_app/lib/main.dart index 11c34dce7..57b082ea0 100644 --- a/examples/travel_app/lib/main.dart +++ b/examples/travel_app/lib/main.dart @@ -4,16 +4,12 @@ // Be sure to uncomment these Firebase initialization code and these imports // if using Firebase AI. -import 'package:firebase_app_check/firebase_app_check.dart'; -import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:genui/genui.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart' - show FirebaseAiContentGenerator; import 'package:logging/logging.dart'; +import 'src/ai_client/ai_client.dart'; import 'src/catalog.dart'; -import 'src/config/configuration.dart'; import 'src/travel_planner_page.dart'; // If you want to convert to using Firebase AI, run: @@ -30,21 +26,8 @@ import 'src/travel_planner_page.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Only initialize Firebase if we are using the Firebase backend. - if (aiBackend == AiBackend.firebase) { - await Firebase.initializeApp( - // UNCOMMENT_FOR_FIREBASE (See top of file for details) - // options: DefaultFirebaseOptions.currentPlatform, - ); - await FirebaseAppCheck.instance.activate( - providerApple: const AppleDebugProvider(), - providerAndroid: const AndroidDebugProvider(), - providerWeb: ReCaptchaV3Provider('debug'), - ); - } - await loadImagesJson(); - configureGenUiLogging(level: Level.ALL); + configureLogging(level: Level.ALL); runApp(const TravelApp()); } @@ -59,38 +42,41 @@ const _title = 'Agentic Travel Inc'; class TravelApp extends StatelessWidget { /// Creates a new [TravelApp]. /// - /// The optional [contentGenerator] can be used to inject a specific AI + /// The optional [aiClient] can be used to inject a specific AI /// client, which is useful for testing with a mock implementation. - const TravelApp({this.contentGenerator, super.key}); + const TravelApp({this.aiClient, super.key}); - final ContentGenerator? contentGenerator; + final AiClient? aiClient; @override Widget build(BuildContext context) { + final colorScheme = ColorScheme.fromSeed(seedColor: Colors.blue); + return MaterialApp( debugShowCheckedModeBanner: false, title: _title, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), + theme: ThemeData(colorScheme: colorScheme), + darkTheme: ThemeData( + colorScheme: colorScheme.copyWith(brightness: Brightness.dark), ), - home: _TravelAppBody(contentGenerator: contentGenerator), + home: _TravelAppBody(aiClient: aiClient), ); } } class _TravelAppBody extends StatelessWidget { - const _TravelAppBody({this.contentGenerator}); + const _TravelAppBody({this.aiClient}); /// The AI client to use for the application. /// - /// If null, a default [FirebaseAiContentGenerator] will be created by the + /// If null, a default client will be created by the /// [TravelPlannerPage]. - final ContentGenerator? contentGenerator; + final AiClient? aiClient; @override Widget build(BuildContext context) { final Map tabs = { - 'Travel': TravelPlannerPage(contentGenerator: contentGenerator), + 'Travel': TravelPlannerPage(aiClient: aiClient), 'Widget Catalog': const CatalogTab(), }; return DefaultTabController( diff --git a/examples/travel_app/lib/src/ai_client/ai_client.dart b/examples/travel_app/lib/src/ai_client/ai_client.dart new file mode 100644 index 000000000..03d2c8d0d --- /dev/null +++ b/examples/travel_app/lib/src/ai_client/ai_client.dart @@ -0,0 +1,34 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:genui/genui.dart'; + +/// An abstract interface for AI clients. +/// +/// This interface defines the contract for communicating with an AI service, +/// regardless of the implementation (e.g., Google Generative AI, fake client). +abstract interface class AiClient { + /// The stream of [A2uiMessage]s received from the AI. + Stream get a2uiMessageStream; + + /// The stream of text chunks received from the AI. + Stream get textResponseStream; + + /// Sends a message to the AI service. + /// + /// [message] is the new message to send. + /// [history] is the history of the conversation so far. + Future sendRequest( + ChatMessage message, { + Iterable? history, + A2UiClientCapabilities? clientCapabilities, + Map? clientDataModel, + CancellationSignal? cancellationSignal, + }); + + /// Dispose of resources. + void dispose(); +} diff --git a/examples/travel_app/lib/src/ai_client/google_content_converter.dart b/examples/travel_app/lib/src/ai_client/google_content_converter.dart new file mode 100644 index 000000000..e0abfa90f --- /dev/null +++ b/examples/travel_app/lib/src/ai_client/google_content_converter.dart @@ -0,0 +1,129 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:genui/genui.dart'; +import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' + as google_ai; +import 'package:google_cloud_protobuf/protobuf.dart' as protobuf; + +/// An exception thrown by this package. +class GoogleAiClientException implements Exception { + /// Creates an [GoogleAiClientException] with the given [message]. + GoogleAiClientException(this.message); + + /// The message associated with the exception. + final String message; + + @override + String toString() => '$GoogleAiClientException: $message'; +} + +/// A class to convert between the generic `ChatMessage` and the `google_ai` +/// specific `Content` classes. +class GoogleContentConverter { + /// Converts a list of [ChatMessage]s to a list of [google_ai.Content]s. + List toGoogleAiContent(Iterable messages) { + final result = []; + for (final message in messages) { + final String? role = switch (message.role) { + ChatMessageRole.user => 'user', + ChatMessageRole.model => 'model', + ChatMessageRole.system => null, + }; + + if (role == null) continue; + + final List parts = _convertParts(message.parts); + if (parts.isNotEmpty) { + result.add(google_ai.Content(role: role, parts: parts)); + } + } + return result; + } + + List _convertParts(List parts) { + final result = []; + for (final part in parts) { + switch (part) { + case TextPart(:final text): + result.add(google_ai.Part(text: text)); + case DataPart(): + if (part.isUiPart) { + final UiPart uiPart = part.asUiPart!; + result.add( + google_ai.Part( + text: uiPart.definition.asContextDescriptionText(), + ), + ); + } else if (part.mimeType == + 'application/vnd.genui.interaction+json') { + result.add(google_ai.Part(text: utf8.decode(part.bytes))); + } else { + // Treat as Blob (image or other) + result.add( + google_ai.Part( + inlineData: google_ai.Blob( + mimeType: part.mimeType, + data: part.bytes, + ), + ), + ); + } + case LinkPart(:final url): + result.add( + google_ai.Part( + fileData: google_ai.FileData(fileUri: url.toString()), + ), + ); + case ToolPart( + :final callId, + :final toolName, + :final arguments, + result: final toolResult, + ): + if (toolResult != null) { + // Tool Result + Map mapResult; + if (toolResult is String) { + try { + mapResult = jsonDecode(toolResult) as Map; + } catch (_) { + mapResult = {'result': toolResult}; + } + } else if (toolResult is Map) { + mapResult = toolResult as Map; + } else { + mapResult = {'result': toolResult}; + } + + result.add( + google_ai.Part( + functionResponse: google_ai.FunctionResponse( + id: callId, + name: '', + response: protobuf.Struct.fromJson(mapResult), + ), + ), + ); + } else { + // Tool Call + result.add( + google_ai.Part( + functionCall: google_ai.FunctionCall( + id: callId, + name: toolName, + args: protobuf.Struct.fromJson(arguments ?? {}), + ), + ), + ); + } + case ThinkingPart(:final text): + result.add(google_ai.Part(text: 'Thinking: $text')); + } + } + return result; + } +} diff --git a/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart b/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart similarity index 57% rename from packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart rename to examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart index 95915916a..a063ea830 100644 --- a/packages/genui_google_generative_ai/lib/src/google_generative_ai_content_generator.dart +++ b/examples/travel_app/lib/src/ai_client/google_generative_ai_client.dart @@ -7,35 +7,56 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:genui/genui.dart'; +import 'package:genui/parsing.dart'; import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' as google_ai; import 'package:google_cloud_protobuf/protobuf.dart' as protobuf; import 'package:json_schema_builder/json_schema_builder.dart' as dsb; +import 'ai_client.dart'; import 'google_content_converter.dart'; import 'google_generative_service_interface.dart'; import 'google_schema_adapter.dart'; +import 'tools.dart'; /// A factory for creating a [GoogleGenerativeServiceInterface]. /// /// This is used to allow for custom service creation, for example, for testing. typedef GenerativeServiceFactory = GoogleGenerativeServiceInterface Function({ - required GoogleGenerativeAiContentGenerator configuration, + required GoogleGenerativeAiClient configuration, }); -/// A [ContentGenerator] that uses the Google Cloud Generative Language API to +/// A client that uses the Google Cloud Generative Language API to /// generate content. -class GoogleGenerativeAiContentGenerator implements ContentGenerator { - /// Creates a [GoogleGenerativeAiContentGenerator] instance with specified +class GoogleGenerativeAiClient implements AiClient { + /// Creates a [GoogleGenerativeAiClient] instance with specified /// configurations. - GoogleGenerativeAiContentGenerator({ + /// + /// The [catalog] is the registry of components that can be dynamically + /// rendered. + /// + /// [systemInstruction] is an optional instruction to guide the model's + /// behavior. + /// + /// [outputToolName] allows customizing the name of the internal tool used for + /// final output. Defaults to 'provideFinalOutput'. + /// + /// [serviceFactory] is an optional factory for creating the + /// [GoogleGenerativeServiceInterface]. + /// + /// [additionalTools] allows providing extra [AiTool]s to the model. + /// + /// [modelName] is the name of the model to use (e.g., 'models/gemini-pro'). + /// + /// [apiKey] is the API key to use for authentication. + GoogleGenerativeAiClient({ required this.catalog, this.systemInstruction, this.outputToolName = 'provideFinalOutput', this.serviceFactory = defaultGenerativeServiceFactory, this.additionalTools = const [], - this.modelName = 'models/gemini-2.5-flash', + this.modelName = 'models/gemini-3-flash-preview', this.apiKey, }); @@ -59,7 +80,7 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { /// This factory function is responsible for instantiating the /// [GoogleGenerativeServiceInterface] used for AI interactions. It allows for /// customization of the service setup, or for providing mock services during - /// testing. The factory receives this [GoogleGenerativeAiContentGenerator] + /// testing. The factory receives this [GoogleGenerativeAiClient] /// instance as configuration. /// /// Defaults to a wrapper for the regular [google_ai.GenerativeService] @@ -69,7 +90,7 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { /// Additional tools to make available to the AI model. final List additionalTools; - /// The model name to use (e.g., 'models/gemini-2.5-flash'). + /// The model name to use (e.g., 'models/gemini-3-flash-preview'). final String modelName; /// The API key to use for authentication. @@ -78,55 +99,71 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { /// The total number of input tokens used by this client. int inputTokenUsage = 0; + /// The total number of output tokens used by this client /// The total number of output tokens used by this client int outputTokenUsage = 0; final _a2uiMessageController = StreamController.broadcast(); final _textResponseController = StreamController.broadcast(); - final _errorController = StreamController.broadcast(); + final _eventController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); final _isProcessing = ValueNotifier(false); + /// A stream of A2UI messages produced by the generator. @override Stream get a2uiMessageStream => _a2uiMessageController.stream; + /// A stream of text responses from the agent. @override Stream get textResponseStream => _textResponseController.stream; - @override - Stream get errorStream => _errorController.stream; + /// A stream of errors from the agent. + Stream get errorStream => _errorController.stream; - @override + /// A stream of events related to the generation process (tool calls, usage, + /// etc.). + Stream get eventStream => _eventController.stream; + + /// Whether the content generator is currently processing a request. ValueListenable get isProcessing => _isProcessing; @override void dispose() { _a2uiMessageController.close(); _textResponseController.close(); + _eventController.close(); _errorController.close(); _isProcessing.dispose(); } + void emitEvent(GenerationEvent event) { + if (!_eventController.isClosed) { + _eventController.add(event); + } + } + + /// Sends a request to the AI model. @override Future sendRequest( ChatMessage message, { Iterable? history, A2UiClientCapabilities? clientCapabilities, + Map? clientDataModel, + CancellationSignal? cancellationSignal, }) async { _isProcessing.value = true; try { final messages = [...?history, message]; - final response = await _generate( + await _generate( messages: messages, - // This turns on forced function calling. - outputSchema: dsb.S.object(properties: {'response': dsb.S.string()}), + cancellationSignal: cancellationSignal, + clientDataModel: clientDataModel, ); - // Convert any response to a text response to the user. - if (response is Map && response.containsKey('response')) { - _textResponseController.add(response['response']! as String); - } - } catch (e, st) { - genUiLogger.severe('Error generating content', e, st); - _errorController.add(ContentGeneratorError(e, st)); + } on CancellationException { + genUiLogger.info('Request cancelled'); + } catch (exception, stackTrace) { + genUiLogger.severe('Error generating content', exception, stackTrace); + _errorController.add(exception); } finally { _isProcessing.value = false; } @@ -135,10 +172,10 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { /// The default factory function for creating a [google_ai.GenerativeService]. /// /// This function instantiates a standard [google_ai.GenerativeService] using - /// the `apiKey` from the provided [GoogleGenerativeAiContentGenerator] + /// the `apiKey` from the provided [GoogleGenerativeAiClient] /// `configuration`. static GoogleGenerativeServiceInterface defaultGenerativeServiceFactory({ - required GoogleGenerativeAiContentGenerator configuration, + required GoogleGenerativeAiClient configuration, }) { return GoogleGenerativeServiceWrapper( google_ai.GenerativeService.fromApiKey(configuration.apiKey), @@ -157,7 +194,8 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { '${isForcedToolCalling ? ' with forced tool calling' : ''}', ); // Create an "output" tool that copies its args into the output. - final finalOutputAiTool = isForcedToolCalling + final DynamicAiTool>? finalOutputAiTool = + isForcedToolCalling ? DynamicAiTool>( name: outputToolName, description: @@ -169,7 +207,7 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { ) : null; - final allTools = isForcedToolCalling + final List> allTools = isForcedToolCalling ? [...availableTools, finalOutputAiTool!] : availableTools; genUiLogger.fine( @@ -192,10 +230,12 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { } final functionDeclarations = []; - for (final tool in uniqueAiToolsByName.values) { + for (final AiTool tool in uniqueAiToolsByName.values) { google_ai.Schema? adaptedParameters; if (tool.parameters != null) { - final result = adapter.adapt(tool.parameters!); + final GoogleSchemaAdapterResult result = adapter.adapt( + tool.parameters!, + ); if (result.errors.isNotEmpty) { genUiLogger.warning( 'Errors adapting parameters for tool ${tool.name}: ' @@ -226,7 +266,7 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { '${functionDeclarations.map((d) => d.name).join(', ')}', ); - final tools = functionDeclarations.isNotEmpty + final List? tools = functionDeclarations.isNotEmpty ? [google_ai.Tool(functionDeclarations: functionDeclarations)] : null; @@ -264,19 +304,70 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { genUiLogger.fine( 'Processing function call: ${call.name} with args: ${call.args}', ); + + // Convert Struct args to Map for easier handling + final Map argsMap = + call.args?.toJson() as Map? ?? {}; + + // Intercept tool call + // final toolAction = await interceptToolCall(call.name, argsMap); + // Tool interception removed with ContentGeneratorMixin for now, + // or needs reimplementation + // default proceed: + // if (toolAction is ToolActionCancel) ... + + /* + if (toolAction is ToolActionCancel) { + genUiLogger.info('Tool call ${call.name} cancelled by interceptor.'); + // Return an error/cancellation message to the model so it knows what happened. + functionResponseParts.add( + google_ai.Part( + functionResponse: google_ai.FunctionResponse( + id: call.id, + name: call.name, + response: protobuf.Struct.fromJson({ + 'error': 'Tool call cancelled by client.', + }), + ), + ), + ); + continue; + } else if (toolAction is ToolActionMock) { + genUiLogger.info( + 'Tool call ${call.name} mocked by interceptor ' + 'with result: ${toolAction.result}', + ); + functionResponseParts.add( + google_ai.Part( + functionResponse: google_ai.FunctionResponse( + id: call.id, + name: call.name, + // Ensure result is a Map for Struct conversion if possible, + // otherwise wrap it or handle it. + // protobuf.Struct expects Map. + response: protobuf.Struct.fromJson( + toolAction.result as Map, + ), + ), + ), + ); + continue; + } + */ + + // ToolActionProceed falls through here + if (isForcedToolCalling && call.name == outputToolName) { try { - // Convert Struct args to Map to extract output - final argsMap = call.args?.toJson() as Map?; - capturedResult = argsMap?['output']; + capturedResult = argsMap['output']; genUiLogger.fine( 'Captured final output from tool "$outputToolName".', ); - } catch (exception, stack) { + } catch (exception, stackTrace) { genUiLogger.severe( 'Unable to read output: $call [${call.args}]', exception, - stack, + stackTrace, ); } genUiLogger.info( @@ -286,30 +377,44 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { break; } - final aiTool = availableTools.firstWhere( + final AiTool aiTool = availableTools.firstWhere( (t) => t.name == call.name || t.fullName == call.name, orElse: () => throw Exception('Unknown tool ${call.name} called.'), ); + + // Emit ToolStartEvent + emitEvent(ToolStartEvent(toolName: aiTool.name, args: argsMap)); + Map toolResult; + final startTime = DateTime.now(); try { genUiLogger.fine('Invoking tool: ${aiTool.name}'); - // Convert Struct args to Map for tool invocation - final argsMap = call.args?.toJson() as Map? ?? {}; toolResult = await aiTool.invoke(argsMap); genUiLogger.info( 'Invoked tool ${aiTool.name} with args $argsMap. ' 'Result: $toolResult', ); - } catch (exception, stack) { + } catch (exception, stackTrace) { genUiLogger.severe( 'Error invoking tool ${aiTool.name} with args ${call.args}: ', exception, - stack, + stackTrace, ); toolResult = { 'error': 'Tool ${aiTool.name} failed to execute: $exception', }; } + final Duration duration = DateTime.now().difference(startTime); + + // Emit ToolEndEvent + emitEvent( + ToolEndEvent( + toolName: aiTool.name, + result: toolResult, + duration: duration, + ), + ); + functionResponseParts.add( google_ai.Part( functionResponse: google_ai.FunctionResponse( @@ -332,62 +437,78 @@ class GoogleGenerativeAiContentGenerator implements ContentGenerator { Future _generate({ required Iterable messages, - dsb.Schema? outputSchema, + CancellationSignal? cancellationSignal, + Map? clientDataModel, }) async { - final isForcedToolCalling = outputSchema != null; final converter = GoogleContentConverter(); final adapter = GoogleSchemaAdapter(); - final service = serviceFactory(configuration: this); + final GoogleGenerativeServiceInterface service = serviceFactory( + configuration: this, + ); try { - final availableTools = [ - SurfaceUpdateTool( - handleMessage: _a2uiMessageController.add, - catalog: catalog, - ), - BeginRenderingTool( - handleMessage: _a2uiMessageController.add, - catalogId: catalog.catalogId, - ), - DeleteSurfaceTool(handleMessage: _a2uiMessageController.add), - ...additionalTools, - ]; + // Remove default tools if they are overridden by additionalTools + final List> availableTools = [...additionalTools]; // A local copy of the incoming messages which is updated with // tool results // as they are generated. - final content = converter.toGoogleAiContent(messages); + final List content = converter.toGoogleAiContent( + messages, + ); - final (:tools, :allowedFunctionNames) = _setupToolsAndFunctions( - isForcedToolCalling: isForcedToolCalling, + final ( + :List? tools, + :Set allowedFunctionNames, + ) = _setupToolsAndFunctions( + isForcedToolCalling: false, availableTools: availableTools, adapter: adapter, - outputSchema: outputSchema, + outputSchema: null, ); var toolUsageCycle = 0; const maxToolUsageCycles = 40; // Safety break for tool loops - Object? capturedResult; // Build system instruction if provided - final systemInstructionContent = systemInstruction != null - ? [ - google_ai.Content( - parts: [google_ai.Part(text: systemInstruction)], - ), - ] + final parts = []; + if (systemInstruction != null) { + parts.add(google_ai.Part(text: systemInstruction)); + } + parts.add( + google_ai.Part( + text: + 'Current Date: ' + '${DateTime.now().toIso8601String().split('T').first}\n' + 'You do not have the ability to execute code. If you need to ' + 'perform calculations, do them yourself.', + ), + ); + parts.add(google_ai.Part(text: BasicCatalogEmbed.basicCatalogRules)); + final String catalogJson = A2uiMessage.a2uiMessageSchema( + catalog, + ).toJson(indent: ' '); + if (clientDataModel != null) { + final String dataString = const JsonEncoder.withIndent( + ' ', + ).convert(clientDataModel); + parts.add(google_ai.Part(text: 'Client Data Model:\n$dataString')); + } + parts.add(google_ai.Part(text: 'A2UI Message Schema:\n$catalogJson')); + + final systemInstructionContent = parts.isNotEmpty + ? [google_ai.Content(role: 'user', parts: parts)] : []; while (toolUsageCycle < maxToolUsageCycles) { - genUiLogger.fine('Starting tool usage cycle ${toolUsageCycle + 1}.'); - if (isForcedToolCalling && capturedResult != null) { - genUiLogger.fine('Captured result found, exiting tool usage loop.'); - break; + if (cancellationSignal?.isCancelled ?? false) { + throw const CancellationException(); } + genUiLogger.fine('Starting tool usage cycle ${toolUsageCycle + 1}.'); toolUsageCycle++; - final concatenatedContents = content + final String concatenatedContents = content .map((c) => jsonEncode(c.toJson())) .join('\n'); @@ -397,6 +518,11 @@ With functions: '${allowedFunctionNames.join(', ')}', ''', ); + final String instructionText = [ + ...systemInstructionContent, + ...content, + ].map((c) => c.parts.map((p) => p.text).join('')).join('\n---\n'); + genUiLogger.fine('Full prompt content: $instructionText'); final inferenceStartTime = DateTime.now(); google_ai.GenerateContentResponse response; try { @@ -404,33 +530,38 @@ With functions: model: modelName, contents: [...systemInstructionContent, ...content], tools: tools ?? [], - toolConfig: isForcedToolCalling + toolConfig: (tools?.isNotEmpty ?? false) ? google_ai.ToolConfig( - functionCallingConfig: google_ai.FunctionCallingConfig( - mode: google_ai.FunctionCallingConfig_Mode.any, - allowedFunctionNames: allowedFunctionNames.toList(), - ), - ) - : google_ai.ToolConfig( functionCallingConfig: google_ai.FunctionCallingConfig( mode: google_ai.FunctionCallingConfig_Mode.auto, ), - ), + ) + : null, ); response = await service.generateContent(request); genUiLogger.finest( 'Raw model response: ${_responseToString(response)}', ); - } catch (e, st) { - genUiLogger.severe('Error from service.generateContent', e, st); - _errorController.add(ContentGeneratorError(e, st)); + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Error from service.generateContent', + exception, + stackTrace, + ); + _errorController.add(exception); rethrow; } - final elapsed = DateTime.now().difference(inferenceStartTime); + final Duration elapsed = DateTime.now().difference(inferenceStartTime); if (response.usageMetadata != null) { inputTokenUsage += response.usageMetadata!.promptTokenCount; outputTokenUsage += response.usageMetadata!.candidatesTokenCount; + emitEvent( + TokenUsageEvent( + inputTokens: response.usageMetadata!.promptTokenCount, + outputTokens: response.usageMetadata!.candidatesTokenCount, + ), + ); } genUiLogger.info( '****** Completed Inference ******\n' @@ -444,13 +575,13 @@ With functions: genUiLogger.warning( 'Response has no candidates: ${response.promptFeedback}', ); - return isForcedToolCalling ? null : ''; + return ''; } - final candidate = response.candidates.first; + final google_ai.Candidate candidate = response.candidates.first; final functionCalls = []; if (candidate.content?.parts != null) { - for (final part in candidate.content!.parts) { + for (final google_ai.Part part in candidate.content!.parts) { if (part.functionCall != null) { functionCalls.add(part.functionCall!); } @@ -459,49 +590,52 @@ With functions: if (functionCalls.isEmpty) { genUiLogger.fine('Model response contained no function calls.'); - if (isForcedToolCalling) { - genUiLogger.warning( - 'Model did not call any function. FinishReason: ' - '${candidate.finishReason}.', - ); - // Extract text from parts - String? text; - if (candidate.content?.parts != null) { - final textParts = candidate.content!.parts - .where((google_ai.Part p) => p.text != null) - .map((google_ai.Part p) => p.text!) - .toList(); - text = textParts.join(''); - } - if (text != null && text.trim().isNotEmpty) { + // Extract text from parts + var text = ''; + if (candidate.content?.parts != null) { + final List textParts = candidate.content!.parts + .where((google_ai.Part p) => p.text != null) + .map((google_ai.Part p) => p.text!) + .toList(); + text = textParts.join(''); + } + if (candidate.content != null) { + content.add(candidate.content!); + } + + // Parse JSON from text. + final List jsonBlocks = JsonBlockParser.parseJsonBlocks( + text, + ); + for (final jsonBlock in jsonBlocks) { + try { + if (jsonBlock is Map) { + // The model sometimes omits the version, so we inject it if + // it's missing. + if (!jsonBlock.containsKey('version')) { + jsonBlock['version'] = 'v0.9'; + } + final message = A2uiMessage.fromJson(jsonBlock); + _a2uiMessageController.add(message); + genUiLogger.info( + 'Emitted A2UI message from prompt extraction: $message', + ); + } + } catch (e) { genUiLogger.warning( - 'Model returned direct text instead of a tool call. ' - 'This might be an error or unexpected AI behavior for ' - 'forced tool calling.', + 'Failed to parse extracted JSON as A2uiMessage: $e', ); } - genUiLogger.fine( - 'Model returned text but no function calls with forced tool ' - 'calling, so returning null.', - ); - return null; - } else { - // Extract text from parts - var text = ''; - if (candidate.content?.parts != null) { - final textParts = candidate.content!.parts - .where((google_ai.Part p) => p.text != null) - .map((google_ai.Part p) => p.text!) - .toList(); - text = textParts.join(''); - } - if (candidate.content != null) { - content.add(candidate.content!); - } - genUiLogger.fine('Returning text response: "$text"'); - _textResponseController.add(text); - return text; } + + if (jsonBlocks.isNotEmpty) { + // remove the JSON from the text response + text = JsonBlockParser.stripJsonBlock(text); + } + + genUiLogger.fine('Returning text response: "$text"'); + _textResponseController.add(text); + return text; } genUiLogger.fine( @@ -516,14 +650,18 @@ With functions: 'parts to conversation.', ); - final result = await _processFunctionCalls( + final ({ + Object? capturedResult, + List functionResponseParts, + }) + result = await _processFunctionCalls( functionCalls: functionCalls, - isForcedToolCalling: isForcedToolCalling, + isForcedToolCalling: false, availableTools: availableTools, - capturedResult: capturedResult, + capturedResult: null, ); - capturedResult = result.capturedResult; - final functionResponseParts = result.functionResponseParts; + final List functionResponseParts = + result.functionResponseParts; if (functionResponseParts.isNotEmpty) { content.add( @@ -534,44 +672,14 @@ With functions: 'parts to conversation.', ); } - - // If the model returned a text response, we assume it's the final - // response and we should stop the tool calling loop. - if (!isForcedToolCalling && candidate.content?.parts != null) { - final textParts = candidate.content!.parts - .where((google_ai.Part p) => p.text != null) - .map((google_ai.Part p) => p.text!) - .toList(); - final text = textParts.join(''); - if (text.trim().isNotEmpty) { - genUiLogger.fine( - 'Model returned a text response of "${text.trim()}". ' - 'Exiting tool loop.', - ); - _textResponseController.add(text); - return text; - } - } } - if (isForcedToolCalling) { - if (toolUsageCycle >= maxToolUsageCycles) { - genUiLogger.severe( - 'Error: Tool usage cycle exceeded maximum of $maxToolUsageCycles. ', - 'No final output was produced.', - StackTrace.current, - ); - } - genUiLogger.fine('Exited tool usage loop. Returning captured result.'); - return capturedResult; - } else { - genUiLogger.severe( - 'Error: Tool usage cycle exceeded maximum of $maxToolUsageCycles. ', - 'No final output was produced.', - StackTrace.current, - ); - return ''; - } + genUiLogger.severe( + 'Error: Tool usage cycle exceeded maximum of $maxToolUsageCycles. ', + 'No final output was produced.', + StackTrace.current, + ); + return ''; } finally { service.close(); } @@ -584,7 +692,7 @@ String _responseToString(google_ai.GenerateContentResponse response) { buffer.writeln(' usageMetadata: ${response.usageMetadata},'); buffer.writeln(' promptFeedback: ${response.promptFeedback},'); buffer.writeln(' candidates: ['); - for (final candidate in response.candidates) { + for (final google_ai.Candidate candidate in response.candidates) { buffer.writeln(' Candidate('); buffer.writeln(' finishReason: ${candidate.finishReason},'); buffer.writeln(' finishMessage: "${candidate.finishMessage}",'); @@ -592,16 +700,17 @@ String _responseToString(google_ai.GenerateContentResponse response) { buffer.writeln(' role: "${candidate.content?.role}",'); buffer.writeln(' parts: ['); if (candidate.content?.parts != null) { - for (final part in candidate.content!.parts) { + for (final google_ai.Part part in candidate.content!.parts) { if (part.text != null) { buffer.writeln(' Part(text: "${part.text}"),'); } else if (part.functionCall != null) { buffer.writeln(' Part(functionCall:'); buffer.writeln(' FunctionCall('); buffer.writeln(' name: "${part.functionCall!.name}",'); - final indentedLines = (const JsonEncoder.withIndent(' ').convert( - part.functionCall!.args ?? {}, - )).split('\n').join('\n '); + final String indentedLines = + (const JsonEncoder.withIndent(' ').convert( + part.functionCall!.args ?? {}, + )).split('\n').join('\n '); buffer.writeln(' args: $indentedLines,'); buffer.writeln(' ),'); buffer.writeln(' ),'); diff --git a/packages/genui_google_generative_ai/lib/src/google_generative_service_interface.dart b/examples/travel_app/lib/src/ai_client/google_generative_service_interface.dart similarity index 100% rename from packages/genui_google_generative_ai/lib/src/google_generative_service_interface.dart rename to examples/travel_app/lib/src/ai_client/google_generative_service_interface.dart diff --git a/packages/genui_google_generative_ai/lib/src/google_schema_adapter.dart b/examples/travel_app/lib/src/ai_client/google_schema_adapter.dart similarity index 96% rename from packages/genui_google_generative_ai/lib/src/google_schema_adapter.dart rename to examples/travel_app/lib/src/ai_client/google_schema_adapter.dart index 7c26201ef..c3fbfb549 100644 --- a/packages/genui_google_generative_ai/lib/src/google_schema_adapter.dart +++ b/examples/travel_app/lib/src/ai_client/google_schema_adapter.dart @@ -70,7 +70,7 @@ class GoogleSchemaAdapter { /// [google_ai.Schema] and a list of any errors that occurred. GoogleSchemaAdapterResult adapt(dsb.Schema schema) { _errors.clear(); - final googleSchema = _adapt(schema, ['#']); + final google_ai.Schema? googleSchema = _adapt(schema, ['#']); return GoogleSchemaAdapterResult(googleSchema, List.unmodifiable(_errors)); } @@ -90,7 +90,7 @@ class GoogleSchemaAdapter { ); } - final type = schema.type; + final Object? type = schema.type; String? typeName; if (type is String) { typeName = type; @@ -161,18 +161,18 @@ class GoogleSchemaAdapter { /// Checks for and logs errors for unsupported global keywords. void checkUnsupportedGlobalKeywords(dsb.Schema schema, List path) { const unsupportedKeywords = { - '\$comment', + r'$comment', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', - '\$defs', - '\$ref', - '\$anchor', - '\$dynamicAnchor', - '\$id', - '\$schema', + r'$defs', + r'$ref', + r'$anchor', + r'$dynamicAnchor', + r'$id', + r'$schema', 'allOf', 'oneOf', 'not', @@ -200,9 +200,13 @@ class GoogleSchemaAdapter { final objectSchema = dsb.ObjectSchema.fromMap(dsbSchema.value); final properties = {}; if (objectSchema.properties != null) { - for (final entry in objectSchema.properties!.entries) { - final propertyPath = [...path, 'properties', entry.key]; - final adaptedProperty = _adapt(entry.value, propertyPath); + for (final MapEntry entry + in objectSchema.properties!.entries) { + final List propertyPath = [...path, 'properties', entry.key]; + final google_ai.Schema? adaptedProperty = _adapt( + entry.value, + propertyPath, + ); if (adaptedProperty != null) { properties[entry.key] = adaptedProperty; } @@ -289,7 +293,7 @@ class GoogleSchemaAdapter { } final itemsPath = [...path, 'items']; - final adaptedItems = _adapt(listSchema.items!, itemsPath); + final google_ai.Schema? adaptedItems = _adapt(listSchema.items!, itemsPath); if (adaptedItems == null) { return null; } diff --git a/packages/genui/lib/src/model/tools.dart b/examples/travel_app/lib/src/ai_client/tools.dart similarity index 96% rename from packages/genui/lib/src/model/tools.dart rename to examples/travel_app/lib/src/ai_client/tools.dart index 52f0e7413..db318b287 100644 --- a/packages/genui/lib/src/model/tools.dart +++ b/examples/travel_app/lib/src/ai_client/tools.dart @@ -2,15 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:genui/genui.dart' show JsonMap; import 'package:json_schema_builder/json_schema_builder.dart'; -import '../primitives/simple_items.dart'; - -/// Key used in schema definition to specify the component ID. -/// -/// This key is used in prompts. -const surfaceIdKey = 'surfaceId'; - /// Abstract base class for defining tools that an AI agent can invoke. /// /// An [AiTool] represents a capability that the AI can use to interact with the diff --git a/examples/travel_app/lib/src/catalog.dart b/examples/travel_app/lib/src/catalog.dart index 987ca7e13..267e3d63b 100644 --- a/examples/travel_app/lib/src/catalog.dart +++ b/examples/travel_app/lib/src/catalog.dart @@ -19,16 +19,16 @@ import 'catalog/travel_carousel.dart'; /// Defines the collection of UI components that the generative AI model can use /// to construct the user interface for the travel app. /// -/// This catalog includes a mix of core widgets (like [CoreCatalogItems.column] -/// and [CoreCatalogItems.text]) and custom, domain-specific widgets tailored +/// This catalog includes a mix of core widgets (like [BasicCatalogItems.column] +/// and [BasicCatalogItems.text]) and custom, domain-specific widgets tailored /// for a travel planning experience, such as [travelCarousel], [itinerary], /// and [inputGroup]. The AI selects from these components to build a dynamic /// and interactive UI in response to user prompts. final Catalog travelAppCatalog = Catalog([ - CoreCatalogItems.button, - CoreCatalogItems.column, - CoreCatalogItems.text, - CoreCatalogItems.imageFixedSize, + BasicCatalogItems.button, + BasicCatalogItems.column, + BasicCatalogItems.text, + BasicCatalogItems.image, checkboxFilterChipsInput, dateInputChip, informationCard, @@ -40,4 +40,4 @@ final Catalog travelAppCatalog = Catalog([ textInputChip, trailhead, travelCarousel, -], catalogId: 'example.com:travel_v0'); +], catalogId: basicCatalogId); diff --git a/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart b/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart index 03542cdfd..cd7974607 100644 --- a/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart +++ b/examples/travel_app/lib/src/catalog/checkbox_filter_chips_input.dart @@ -18,6 +18,7 @@ final _schema = S.object( 'A chip used to choose from a set of options where *more than one* ' 'option can be chosen. This *must* be placed inside an InputGroup.', properties: { + 'component': S.string(enumValues: ['CheckboxFilterChipsInput']), 'chipLabel': S.string( description: 'The title of the filter chip e.g. "amenities" or "dietary ' @@ -37,7 +38,7 @@ final _schema = S.object( 'initially. These options must exist in the "options" list.', ), }, - required: ['chipLabel', 'options', 'selectedOptions'], + required: ['component', 'chipLabel', 'options', 'selectedOptions'], ); extension type _CheckboxFilterChipsInputData.fromMap( @@ -58,7 +59,7 @@ extension type _CheckboxFilterChipsInputData.fromMap( String get chipLabel => _json['chipLabel'] as String; List get options => (_json['options'] as List).cast(); String? get iconName => _json['iconName'] as String?; - JsonMap get selectedOptions => _json['selectedOptions'] as JsonMap; + Object get selectedOptions => _json['selectedOptions'] as Object; } /// An interactive chip that allows the user to select multiple options from a @@ -80,23 +81,18 @@ final checkboxFilterChipsInput = CatalogItem( [ { "id": "root", - "component": { - "CheckboxFilterChipsInput": { - "chipLabel": "Amenities", - "options": [ - "Wifi", - "Gym", - "Pool", - "Parking" - ], - "selectedOptions": { - "literalArray": [ - "Wifi", - "Gym" - ] - } - } - } + "component": "CheckboxFilterChipsInput", + "chipLabel": "Amenities", + "options": [ + "Wifi", + "Gym", + "Pool", + "Parking" + ], + "selectedOptions": [ + "Wifi", + "Gym" + ] } ] ''', @@ -121,14 +117,26 @@ final checkboxFilterChipsInput = CatalogItem( } } - final JsonMap selectedOptionsRef = checkboxFilterChipsData.selectedOptions; + final Object selectedOptionsRef = checkboxFilterChipsData.selectedOptions; + final path = + (selectedOptionsRef is Map && selectedOptionsRef.containsKey('path')) + ? selectedOptionsRef['path'] as String + : '${context.id}.value'; + final ValueNotifier?> notifier = context.dataContext - .subscribeToObjectArray(selectedOptionsRef); + .subscribeToObjectArray({'path': path}); return ValueListenableBuilder?>( valueListenable: notifier, builder: (buildContext, currentSelectedValues, child) { - final Set selectedOptionsSet = (currentSelectedValues ?? []) + var effectiveSelections = currentSelectedValues; + if (effectiveSelections == null) { + if (selectedOptionsRef is List) { + effectiveSelections = selectedOptionsRef; + } + } + + final Set selectedOptionsSet = (effectiveSelections ?? []) .cast() .toSet(); return _CheckboxFilterChip( @@ -137,13 +145,7 @@ final checkboxFilterChipsInput = CatalogItem( icon: icon, selectedOptions: selectedOptionsSet, onChanged: (newSelectedOptions) { - final path = selectedOptionsRef['path'] as String?; - if (path != null) { - context.dataContext.update( - DataPath(path), - newSelectedOptions.toList(), - ); - } + context.dataContext.update(path, newSelectedOptions.toList()); }, ); }, diff --git a/examples/travel_app/lib/src/catalog/date_input_chip.dart b/examples/travel_app/lib/src/catalog/date_input_chip.dart index b0eaf3143..a3e61a935 100644 --- a/examples/travel_app/lib/src/catalog/date_input_chip.dart +++ b/examples/travel_app/lib/src/catalog/date_input_chip.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: avoid_dynamic_calls - import 'package:flutter/material.dart'; import 'package:genui/genui.dart'; import 'package:intl/intl.dart'; @@ -11,18 +9,20 @@ import 'package:json_schema_builder/json_schema_builder.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['DateInputChip']), 'value': A2uiSchemas.stringReference( description: 'The initial date of the date picker in yyyy-mm-dd format.', ), 'label': S.string(description: 'Label for the date picker.'), }, + required: ['component'], ); extension type _DatePickerData.fromMap(JsonMap _json) { factory _DatePickerData({JsonMap? value, String? label}) => _DatePickerData.fromMap({'value': value, 'label': label}); - JsonMap? get value => _json['value'] as JsonMap?; + Object? get value => _json['value']; String? get label => _json['label'] as String?; } @@ -116,34 +116,32 @@ final dateInputChip = CatalogItem( [ { "id": "root", - "component": { - "DateInputChip": { - "value": { - "literalString": "1871-07-22" - }, - "label": "Your birth date" - } - } + "component": "DateInputChip", + "value": "1871-07-22", + "label": "Your birth date" } ] ''', ], widgetBuilder: (context) { final datePickerData = _DatePickerData.fromMap(context.data as JsonMap); + final Object? value = datePickerData.value; + final path = value is Map && value.containsKey('path') + ? value['path'] as String + : '${context.id}.value'; final ValueNotifier notifier = context.dataContext - .subscribeToString(datePickerData.value); - final path = datePickerData.value?['path'] as String?; + .subscribeToString({'path': path}); return ValueListenableBuilder( valueListenable: notifier, builder: (buildContext, currentValue, child) { + final String? effectiveValue = + currentValue ?? (value is String ? value : null); return _DateInputChip( - initialValue: currentValue, + initialValue: effectiveValue, label: datePickerData.label, onChanged: (newValue) { - if (path != null) { - context.dataContext.update(DataPath(path), newValue); - } + context.dataContext.update(path, newValue); }, ); }, diff --git a/examples/travel_app/lib/src/catalog/information_card.dart b/examples/travel_app/lib/src/catalog/information_card.dart index 3767641bf..e85cd0b93 100644 --- a/examples/travel_app/lib/src/catalog/information_card.dart +++ b/examples/travel_app/lib/src/catalog/information_card.dart @@ -10,6 +10,7 @@ import '../utils.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['InformationCard']), 'imageChildId': S.string( description: 'The ID of the Image widget to display at the top of the ' @@ -24,7 +25,7 @@ final _schema = S.object( description: 'The body text of the card. This supports markdown.', ), }, - required: ['title', 'body'], + required: ['component', 'title', 'body'], ); extension type _InformationCardData.fromMap(Map _json) { @@ -41,9 +42,9 @@ extension type _InformationCardData.fromMap(Map _json) { }); String? get imageChildId => _json['imageChildId'] as String?; - JsonMap get title => _json['title'] as JsonMap; - JsonMap? get subtitle => _json['subtitle'] as JsonMap?; - JsonMap get body => _json['body'] as JsonMap; + Object get title => _json['title'] as Object; + Object? get subtitle => _json['subtitle']; + Object get body => _json['body'] as Object; } final informationCard = CatalogItem( @@ -54,30 +55,16 @@ final informationCard = CatalogItem( [ { "id": "root", - "component": { - "InformationCard": { - "title": { - "literalString": "Beautiful Scenery" - }, - "subtitle": { - "literalString": "A stunning view" - }, - "body": { - "literalString": "This is a beautiful place to visit in the summer." - }, - "imageChildId": "image1" - } - } + "component": "InformationCard", + "title": "Beautiful Scenery", + "subtitle": "A stunning view", + "body": "This is a beautiful place to visit in the summer.", + "imageChildId": "image1" }, { "id": "image1", - "component": { - "Image": { - "url": { - "literalString": "assets/travel_images/canyonlands_national_park_utah.jpg" - } - } - } + "component": "Image", + "url": "assets/travel_images/canyonlands_national_park_utah.jpg" } ] ''', diff --git a/examples/travel_app/lib/src/catalog/input_group.dart b/examples/travel_app/lib/src/catalog/input_group.dart index ed16a7c95..a29342465 100644 --- a/examples/travel_app/lib/src/catalog/input_group.dart +++ b/examples/travel_app/lib/src/catalog/input_group.dart @@ -8,6 +8,7 @@ import 'package:json_schema_builder/json_schema_builder.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['InputGroup']), 'submitLabel': A2uiSchemas.stringReference( description: 'The label for the submit button.', ), @@ -25,7 +26,7 @@ final _schema = S.object( 'know what the user has selected.', ), }, - required: ['submitLabel', 'children', 'action'], + required: ['component', 'submitLabel', 'children', 'action'], ); extension type _InputGroupData.fromMap(Map _json) { @@ -39,7 +40,7 @@ extension type _InputGroupData.fromMap(Map _json) { 'action': action, }); - JsonMap get submitLabel => _json['submitLabel'] as JsonMap; + Object get submitLabel => _json['submitLabel'] as Object; List get children => (_json['children'] as List).cast(); JsonMap get action => _json['action'] as JsonMap; } @@ -58,60 +59,41 @@ final inputGroup = CatalogItem( [ { "id": "root", - "component": { - "InputGroup": { - "submitLabel": { - "literalString": "Submit" - }, - "children": [ - "check_in", - "check_out", - "text_input1", - "text_input2" - ], - "action": { - "name": "submit_form" - } + "component": "InputGroup", + "submitLabel": "Submit", + "children": [ + "check_in", + "check_out", + "text_input1", + "text_input2" + ], + "action": { + "event": { + "name": "submit_form" } } }, { "id": "check_in", - "component": { - "DateInputChip": { - "value": { - "literalString": "2026-07-22" - }, - "label": "Check-in date" - } - } + "component": "DateInputChip", + "value": "2026-07-22", + "label": "Check-in date" }, { "id": "check_out", - "component": { - "DateInputChip": { - "label": "Check-out date" - } - } + "component": "DateInputChip", + "label": "Check-out date" }, { "id": "text_input1", - "component": { - "TextInputChip": { - "value": { - "literalString": "John Doe" - }, - "label": "Enter your name" - } - } + "component": "TextInputChip", + "value": "John Doe", + "label": "Enter your name" }, { "id": "text_input2", - "component": { - "TextInputChip": { - "label": "Enter your friend's name" - } - } + "component": "TextInputChip", + "label": "Enter your friend's name" } ] ''', @@ -128,9 +110,9 @@ final inputGroup = CatalogItem( final List children = inputGroupData.children; final JsonMap actionData = inputGroupData.action; - final name = actionData['name'] as String; - final List contextDefinition = - (actionData['context'] as List?) ?? []; + final event = actionData['event'] as JsonMap?; + final String name = event?['name'] as String? ?? 'unknown'; + final contextDefinition = event?['context'] as JsonMap?; return Card( color: Theme.of(itemContext.buildContext).colorScheme.primaryContainer, diff --git a/examples/travel_app/lib/src/catalog/itinerary.dart b/examples/travel_app/lib/src/catalog/itinerary.dart index d28b4eb51..d1f87343b 100644 --- a/examples/travel_app/lib/src/catalog/itinerary.dart +++ b/examples/travel_app/lib/src/catalog/itinerary.dart @@ -16,6 +16,7 @@ enum ItineraryEntryStatus { noBookingRequired, choiceRequired, chosen } final _schema = S.object( description: 'Widget to show an itinerary or a plan for travel.', properties: { + 'component': S.string(enumValues: ['Itinerary']), 'title': A2uiSchemas.stringReference( description: 'The title of the itinerary.', ), @@ -114,31 +115,31 @@ final _schema = S.object( ), ), }, - required: ['title', 'subheading', 'imageChildId', 'days'], + required: ['component', 'title', 'subheading', 'imageChildId', 'days'], ); extension type _ItineraryData.fromMap(Map _json) { - JsonMap get title => _json['title'] as JsonMap; - JsonMap get subheading => _json['subheading'] as JsonMap; + Object get title => _json['title'] as Object; + Object get subheading => _json['subheading'] as Object; String get imageChildId => _json['imageChildId'] as String; List get days => (_json['days'] as List).cast(); } extension type _ItineraryDayData.fromMap(Map _json) { - JsonMap get title => _json['title'] as JsonMap; - JsonMap get subtitle => _json['subtitle'] as JsonMap; - JsonMap get description => _json['description'] as JsonMap; + Object get title => _json['title'] as Object; + Object get subtitle => _json['subtitle'] as Object; + Object get description => _json['description'] as Object; String get imageChildId => _json['imageChildId'] as String; List get entries => (_json['entries'] as List).cast(); } extension type _ItineraryEntryData.fromMap(Map _json) { - JsonMap get title => _json['title'] as JsonMap; - JsonMap? get subtitle => _json['subtitle'] as JsonMap?; - JsonMap get bodyText => _json['bodyText'] as JsonMap; - JsonMap? get address => _json['address'] as JsonMap?; - JsonMap get time => _json['time'] as JsonMap; - JsonMap? get totalCost => _json['totalCost'] as JsonMap?; + Object get title => _json['title'] as Object; + Object? get subtitle => _json['subtitle']; + Object get bodyText => _json['bodyText'] as Object; + Object? get address => _json['address']; + Object get time => _json['time'] as Object; + Object? get totalCost => _json['totalCost']; ItineraryEntryType get type => ItineraryEntryType.values.byName(_json['type'] as String); ItineraryEntryStatus get status => @@ -155,66 +156,37 @@ final itinerary = CatalogItem( [ { "id": "root", - "component": { - "Itinerary": { - "title": { - "literalString": "My Awesome Trip" - }, - "subheading": { - "literalString": "A 3-day adventure" - }, - "imageChildId": "image1", - "days": [ + "component": "Itinerary", + "title": "My Awesome Trip", + "subheading": "A 3-day adventure", + "imageChildId": "image1", + "days": [ + { + "title": "Day 1", + "subtitle": "Arrival and Exploration", + "description": "Welcome to the city!", + "imageChildId": "image2", + "entries": [ { - "title": { - "literalString": "Day 1" - }, - "subtitle": { - "literalString": "Arrival and Exploration" - }, - "description": { - "literalString": "Welcome to the city!" - }, - "imageChildId": "image2", - "entries": [ - { - "title": { - "literalString": "Check-in to Hotel" - }, - "bodyText": { - "literalString": "Check-in to your hotel and relax." - }, - "time": { - "literalString": "3:00 PM" - }, - "type": "accommodation", - "status": "noBookingRequired" - } - ] + "title": "Check-in to Hotel", + "bodyText": "Check-in to your hotel and relax.", + "time": "3:00 PM", + "type": "accommodation", + "status": "noBookingRequired" } ] } - } + ] }, { "id": "image1", - "component": { - "Image": { - "url": { - "literalString": "assets/travel_images/canyonlands_national_park_utah.jpg" - } - } - } + "component": "Image", + "url": "assets/travel_images/canyonlands_national_park_utah.jpg" }, { "id": "image2", - "component": { - "Image": { - "url": { - "literalString": "assets/travel_images/brooklyn_bridge_new_york.jpg" - } - } - } + "component": "Image", + "url": "assets/travel_images/brooklyn_bridge_new_york.jpg" } ] ''', @@ -549,10 +521,13 @@ class _ItineraryEntry extends StatelessWidget { if (actionData == null) { return; } - final actionName = actionData['name'] as String; - final List contextDefinition = - (actionData['context'] as List?) ?? - []; + final event = actionData['event'] as JsonMap?; + if (event == null) { + return; + } + final actionName = event['name'] as String; + final contextDefinition = + event['context'] as JsonMap?; final JsonMap resolvedContext = resolveContext( dataContext, contextDefinition, diff --git a/examples/travel_app/lib/src/catalog/listings_booker.dart b/examples/travel_app/lib/src/catalog/listings_booker.dart index f279ddf1d..8af231272 100644 --- a/examples/travel_app/lib/src/catalog/listings_booker.dart +++ b/examples/travel_app/lib/src/catalog/listings_booker.dart @@ -15,6 +15,7 @@ import '../tools/booking/model.dart'; final _schema = S.object( description: 'A widget to select among a set of listings.', properties: { + 'component': S.string(enumValues: ['ListingsBooker']), 'listingSelectionIds': S.list( description: 'Listings to select among.', items: S.string(), @@ -29,7 +30,7 @@ final _schema = S.object( 'the key "listingSelectionId".', ), }, - required: ['listingSelectionIds'], + required: ['component', 'listingSelectionIds'], ); extension type _ListingsBookerData.fromMap(Map _json) { @@ -45,7 +46,7 @@ extension type _ListingsBookerData.fromMap(Map _json) { List get listingSelectionIds => (_json['listingSelectionIds'] as List).cast(); - JsonMap get itineraryName => _json['itineraryName'] as JsonMap; + Object get itineraryName => _json['itineraryName'] as Object; JsonMap? get modifyAction => _json['modifyAction'] as JsonMap?; } @@ -99,12 +100,9 @@ final listingsBooker = CatalogItem( return jsonEncode([ { 'id': 'root', - 'component': { - 'ListingsBooker': { - 'listingSelectionIds': [listingSelectionId1, listingSelectionId2], - 'itineraryName': {'literalString': 'Dart and Flutter deep dive'}, - }, - }, + 'component': 'ListingsBooker', + 'listingSelectionIds': [listingSelectionId1, listingSelectionId2], + 'itineraryName': 'Dart and Flutter deep dive', }, ]); }, @@ -333,9 +331,8 @@ class _ListingsBookerState extends State<_ListingsBooker> { return; } final actionName = actionData['name'] as String; - final List contextDefinition = - (actionData['context'] as List?) ?? - []; + final contextDefinition = + actionData['context'] as JsonMap?; final JsonMap resolvedContext = resolveContext( widget.dataContext, contextDefinition, diff --git a/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart b/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart index 3a5c4c91b..6c8fa133e 100644 --- a/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart +++ b/examples/travel_app/lib/src/catalog/options_filter_chip_input.dart @@ -16,6 +16,7 @@ final _schema = S.object( 'A chip used to choose from a set of mutually exclusive ' 'options. This *must* be placed inside an InputGroup.', properties: { + 'component': S.string(enumValues: ['OptionsFilterChipInput']), 'chipLabel': S.string( description: 'The title of the filter chip e.g. "budget" or "activity type" ' @@ -36,7 +37,7 @@ final _schema = S.object( 'option must exist in the "options" list.', ), }, - required: ['chipLabel', 'options'], + required: ['component', 'chipLabel', 'options'], ); extension type _OptionsFilterChipInputData.fromMap(Map _json) { @@ -55,7 +56,9 @@ extension type _OptionsFilterChipInputData.fromMap(Map _json) { String get chipLabel => _json['chipLabel'] as String; List get options => (_json['options'] as List).cast(); String? get iconName => _json['iconName'] as String?; - JsonMap? get value => _json['value'] as JsonMap?; + JsonMap? get value => + _json['value'] is Map ? _json['value'] as JsonMap : null; + Object? get rawValue => _json['value']; } /// An interactive chip that allows the user to select a single option from a @@ -76,19 +79,10 @@ final optionsFilterChipInput = CatalogItem( [ { "id": "root", - "component": { - "OptionsFilterChipInput": { - "chipLabel": "Budget", - "options": [ - "\$", - "\$\$", - "\$\$\$" - ], - "value": { - "literalString": "\$\$" - } - } - } + "component": "OptionsFilterChipInput", + "chipLabel": "Budget", + "options": ["Low", "Medium", "High"], + "value": "Medium" } ] ''', @@ -108,22 +102,32 @@ final optionsFilterChipInput = CatalogItem( } } - final JsonMap? valueRef = optionsFilterChipData.value; - final path = valueRef?['path'] as String?; + final Object? valueRef = optionsFilterChipData.rawValue; + // If the value is a literal, we still want to bind to a path so that we can + // update the model (and the UI) when the user changes the value. + final path = valueRef is Map && valueRef.containsKey('path') + ? valueRef['path'] as String + : '${context.id}.value'; + // Always subscribe to the path, even if we have a literal value. final ValueNotifier notifier = context.dataContext - .subscribeToString(valueRef); + .subscribeToString({'path': path}); return ValueListenableBuilder( valueListenable: notifier, builder: (builderContext, currentValue, child) { + // If the data model is empty at the path, fall back to the literal + // value provided in the component definition. + final String? effectiveValue = + currentValue ?? (valueRef is String ? valueRef : null); + return _OptionsFilterChip( chipLabel: optionsFilterChipData.chipLabel, options: optionsFilterChipData.options, icon: icon, - value: currentValue, + value: effectiveValue, onChanged: (newValue) { - if (path != null && newValue != null) { - context.dataContext.update(DataPath(path), newValue); + if (newValue != null) { + context.dataContext.update(path, newValue); } }, ); diff --git a/examples/travel_app/lib/src/catalog/tabbed_sections.dart b/examples/travel_app/lib/src/catalog/tabbed_sections.dart index f2eed8fc0..87d80ee3d 100644 --- a/examples/travel_app/lib/src/catalog/tabbed_sections.dart +++ b/examples/travel_app/lib/src/catalog/tabbed_sections.dart @@ -8,6 +8,7 @@ import 'package:json_schema_builder/json_schema_builder.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['TabbedSections']), 'sections': S.list( description: 'A list of sections to display as tabs.', items: S.object( @@ -23,7 +24,7 @@ final _schema = S.object( ), ), }, - required: ['sections'], + required: ['component', 'sections'], ); extension type _TabbedSectionsData.fromMap(Map _json) { @@ -36,12 +37,10 @@ extension type _TabbedSectionsData.fromMap(Map _json) { } extension type _TabSectionItemData.fromMap(Map _json) { - factory _TabSectionItemData({ - required Map title, - required String child, - }) => _TabSectionItemData.fromMap({'child': child, 'title': title}); + factory _TabSectionItemData({required Object title, required String child}) => + _TabSectionItemData.fromMap({'child': child, 'title': title}); - Map get title => _json['title'] as Map; + Object get title => _json['title'] as Object; String get childId => _json['child'] as String; } @@ -60,44 +59,27 @@ final tabbedSections = CatalogItem( [ { "id": "root", - "component": { - "TabbedSections": { - "sections": [ - { - "title": { - "literalString": "Tab 1" - }, - "child": "tab1_content" - }, - { - "title": { - "literalString": "Tab 2" - }, - "child": "tab2_content" - } - ] + "component": "TabbedSections", + "sections": [ + { + "title": "Tab 1", + "child": "tab1_content" + }, + { + "title": "Tab 2", + "child": "tab2_content" } - } + ] }, { "id": "tab1_content", - "component": { - "Text": { - "text": { - "literalString": "This is the content of Tab 1." - } - } - } + "component": "Text", + "text": "This is the content of Tab 1." }, { "id": "tab2_content", - "component": { - "Text": { - "text": { - "literalString": "This is the content of Tab 2." - } - } - } + "component": "Text", + "text": "This is the content of Tab 2." } ] ''', diff --git a/examples/travel_app/lib/src/catalog/text_input_chip.dart b/examples/travel_app/lib/src/catalog/text_input_chip.dart index 058ef6e3d..6298fa84f 100644 --- a/examples/travel_app/lib/src/catalog/text_input_chip.dart +++ b/examples/travel_app/lib/src/catalog/text_input_chip.dart @@ -11,6 +11,7 @@ final _schema = S.object( 'An input chip used to ask the user to enter free text, e.g. to ' 'select a destination. This should only be used inside an InputGroup.', properties: { + 'component': S.string(enumValues: ['TextInputChip']), 'label': S.string(description: 'The label for the text input chip.'), 'value': A2uiSchemas.stringReference( description: 'The initial value for the text input.', @@ -19,7 +20,7 @@ final _schema = S.object( description: 'Whether the text should be obscured (e.g., for passwords).', ), }, - required: ['label'], + required: ['component', 'label'], ); extension type _TextInputChipData.fromMap(Map _json) { @@ -34,7 +35,7 @@ extension type _TextInputChipData.fromMap(Map _json) { }); String get label => _json['label'] as String; - JsonMap? get value => _json['value'] as JsonMap?; + Object? get value => _json['value']; bool get obscured => _json['obscured'] as bool? ?? false; } @@ -46,14 +47,9 @@ final textInputChip = CatalogItem( [ { "id": "root", - "component": { - "TextInputChip": { - "value": { - "literalString": "John Doe" - }, - "label": "Enter your name" - } - } + "component": "TextInputChip", + "value": "John Doe", + "label": "Enter your name" } ] ''', @@ -61,12 +57,9 @@ final textInputChip = CatalogItem( [ { "id": "root", - "component": { - "TextInputChip": { - "label": "Enter your password", - "obscured": true - } - } + "component": "TextInputChip", + "label": "Enter your password", + "obscured": true } ] ''', @@ -76,22 +69,24 @@ final textInputChip = CatalogItem( context.data as Map, ); - final JsonMap? valueRef = textInputChipData.value; - final path = valueRef?['path'] as String?; + final Object? valueRef = textInputChipData.value; + final path = valueRef is Map && valueRef.containsKey('path') + ? valueRef['path'] as String + : '${context.id}.value'; final ValueNotifier notifier = context.dataContext - .subscribeToString(valueRef); + .subscribeToString({'path': path}); return ValueListenableBuilder( valueListenable: notifier, builder: (builderContext, currentValue, child) { + final String? effectiveValue = + currentValue ?? (valueRef is String ? valueRef : null); return _TextInputChip( label: textInputChipData.label, - value: currentValue, + value: effectiveValue, obscured: textInputChipData.obscured, onChanged: (newValue) { - if (path != null) { - context.dataContext.update(DataPath(path), newValue); - } + context.dataContext.update(path, newValue); }, ); }, diff --git a/examples/travel_app/lib/src/catalog/trailhead.dart b/examples/travel_app/lib/src/catalog/trailhead.dart index d2ed234b9..863d9e54b 100644 --- a/examples/travel_app/lib/src/catalog/trailhead.dart +++ b/examples/travel_app/lib/src/catalog/trailhead.dart @@ -8,6 +8,7 @@ import 'package:json_schema_builder/json_schema_builder.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['Trailhead']), 'topics': S.list( description: 'A list of topics to display as chips.', items: A2uiSchemas.stringReference(description: 'A topic to explore.'), @@ -18,7 +19,7 @@ final _schema = S.object( 'will be added to the context with the key "topic".', ), }, - required: ['topics', 'action'], + required: ['component', 'topics', 'action'], ); extension type _TrailheadData.fromMap(Map _json) { @@ -27,7 +28,7 @@ extension type _TrailheadData.fromMap(Map _json) { required JsonMap action, }) => _TrailheadData.fromMap({'topics': topics, 'action': action}); - List get topics => (_json['topics'] as List).cast(); + List get topics => (_json['topics'] as List).cast(); JsonMap get action => _json['action'] as JsonMap; } @@ -48,22 +49,15 @@ final trailhead = CatalogItem( [ { "id": "root", - "component": { - "Trailhead": { - "topics": [ - { - "literalString": "Topic 1" - }, - { - "literalString": "Topic 2" - }, - { - "literalString": "Topic 3" - } - ], - "action": { - "name": "select_topic" - } + "component": "Trailhead", + "topics": [ + "Topic 1", + "Topic 2", + "Topic 3" + ], + "action": { + "event": { + "name": "select_topic" } } } @@ -93,7 +87,7 @@ class _Trailhead extends StatelessWidget { required this.dataContext, }); - final List topics; + final List topics; final JsonMap action; final String widgetId; final DispatchEventCallback dispatchEvent; @@ -120,9 +114,9 @@ class _Trailhead extends StatelessWidget { return InputChip( label: Text(topic), onPressed: () { - final name = action['name'] as String; - final List contextDefinition = - (action['context'] as List?) ?? []; + final event = action['event'] as JsonMap?; + final String name = event?['name'] as String? ?? 'unknown'; + final contextDefinition = event?['context'] as JsonMap?; final JsonMap resolvedContext = resolveContext( dataContext, contextDefinition, diff --git a/examples/travel_app/lib/src/catalog/travel_carousel.dart b/examples/travel_app/lib/src/catalog/travel_carousel.dart index 2cb267f43..3fe7eb44c 100644 --- a/examples/travel_app/lib/src/catalog/travel_carousel.dart +++ b/examples/travel_app/lib/src/catalog/travel_carousel.dart @@ -14,6 +14,7 @@ import '../tools/booking/model.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['TravelCarousel']), 'title': A2uiSchemas.stringReference( description: 'An optional title to display above the carousel.', ), @@ -50,7 +51,7 @@ final _schema = S.object( ), ), }, - required: ['items'], + required: ['component', 'items'], ); /// A widget that presents a horizontally scrolling list of tappable items, each @@ -107,7 +108,7 @@ extension type _TravelCarouselData.fromMap(Map _json) { required List> items, }) => _TravelCarouselData.fromMap({'title': ?title, 'items': items}); - JsonMap? get title => _json['title'] as JsonMap?; + Object? get title => _json['title']; Iterable<_TravelCarouselItemSchemaData> get items => (_json['items'] as List) .cast>() .map<_TravelCarouselItemSchemaData>( @@ -130,7 +131,7 @@ extension type _TravelCarouselItemSchemaData.fromMap( 'action': action, }); - JsonMap get description => _json['description'] as JsonMap? ?? {}; + Object get description => _json['description'] as Object; String get imageChildId => _json['imageChildId'] as String? ?? ''; String? get listingSelectionId => _json['listingSelectionId'] as String?; JsonMap get action => _json['action'] as JsonMap? ?? {}; @@ -232,9 +233,9 @@ class _TravelCarouselItem extends StatelessWidget { width: 190, child: InkWell( onTap: () { - final name = data.action['name'] as String; - final List contextDefinition = - (data.action['context'] as List?) ?? []; + final event = data.action['event'] as JsonMap?; + final String name = event?['name'] as String? ?? 'unknown'; + final contextDefinition = event?['context'] as JsonMap?; final JsonMap resolvedContext = resolveContext( dataContext, contextDefinition, @@ -299,42 +300,37 @@ String _hotelExample() { return jsonEncode([ { 'id': 'root', - 'component': { - 'TravelCarousel': { - 'items': [ - { - 'description': {'literalString': hotel1.description}, - 'imageChildId': 'image_1', - 'listingSelectionId': '12345', - 'action': {'name': 'selectHotel'}, - }, - { - 'description': {'literalString': hotel2.description}, - 'imageChildId': 'image_2', - 'listingSelectionId': '12346', - 'action': {'name': 'selectHotel'}, - }, - ], + 'component': 'TravelCarousel', + 'items': [ + { + 'description': hotel1.description, + 'imageChildId': 'image_1', + 'listingSelectionId': '12345', + 'action': { + 'event': {'name': 'selectHotel'}, + }, }, - }, + { + 'description': hotel2.description, + 'imageChildId': 'image_2', + 'listingSelectionId': '12346', + 'action': { + 'event': {'name': 'selectHotel'}, + }, + }, + ], }, { 'id': 'image_1', - 'component': { - 'Image': { - 'fit': 'cover', - 'url': {'literalString': hotel1.images[0]}, - }, - }, + 'component': 'Image', + 'fit': 'cover', + 'url': hotel1.images[0], }, { 'id': 'image_2', - 'component': { - 'Image': { - 'fit': 'cover', - 'url': {'literalString': hotel2.images[0]}, - }, - }, + 'component': 'Image', + 'fit': 'cover', + 'url': hotel2.images[0], }, ]); } @@ -343,115 +339,82 @@ String _inspirationExample() => ''' [ { "id": "root", - "component": { - "Column": { - "children": { - "explicitList": ["inspiration_title", "inspiration_carousel"] - } - } - } + "component": "Column", + "children": ["inspiration_title", "inspiration_carousel"] }, { "id": "inspiration_title", - "component": { - "Text": { - "text": { - "literalString": "Let's plan your dream trip to Greece! What kind of experience are you looking for?" - } - } - } + "component": "Text", + "text": "Let's plan your dream trip to Greece! What kind of experience are you looking for?" }, { "id": "inspiration_carousel", - "component": { - "TravelCarousel": { - "items": [ - { - "description": { - "literalString": "Relaxing Beach Holiday" - }, - "imageChildId": "santorini_beach_image", - "listingSelectionId": "12345", - "action": { - "name": "selectExperience" - } - }, - { - "imageChildId": "akrotiri_fresco_image", - "description": { - "literalString": "Cultural Exploration" - }, - "listingSelectionId": "12346", - "action": { - "name": "selectExperience" - } - }, - { - "imageChildId": "santorini_caldera_image", - "description": { - "literalString": "Adventure & Outdoors" - }, - "listingSelectionId": "12347", - "action": { - "name": "selectExperience" - } - }, - { - "description": { - "literalString": "Foodie Tour" - }, - "imageChildId": "greece_food_image", - "action": { - "name": "selectExperience" - } + "component": "TravelCarousel", + "items": [ + { + "description": "Relaxing Beach Holiday", + "imageChildId": "santorini_beach_image", + "listingSelectionId": "12345", + "action": { + "event": { + "name": "selectExperience" } - ] + } + }, + { + "imageChildId": "akrotiri_fresco_image", + "description": "Cultural Exploration", + "listingSelectionId": "12346", + "action": { + "event": { + "name": "selectExperience" + } + } + }, + { + "imageChildId": "santorini_caldera_image", + "description": "Adventure & Outdoors", + "listingSelectionId": "12347", + "action": { + "event": { + "name": "selectExperience" + } + } + }, + { + "description": "Foodie Tour", + "imageChildId": "greece_food_image", + "action": { + "event": { + "name": "selectExperience" + } + } } - } + ] }, { "id": "santorini_beach_image", - "component": { - "Image": { - "fit": "cover", - "url": { - "literalString": "assets/travel_images/santorini_panorama.jpg" - } - } - } + "component": "Image", + "fit": "cover", + "url": "assets/travel_images/santorini_panorama.jpg" }, { "id": "akrotiri_fresco_image", - "component": { - "Image": { - "fit": "cover", - "url": { - "literalString": "assets/travel_images/akrotiri_spring_fresco_santorini.jpg" - } - } - } + "component": "Image", + "fit": "cover", + "url": "assets/travel_images/akrotiri_spring_fresco_santorini.jpg" }, { "id": "santorini_caldera_image", - "component": { - "Image": { - "url": { - "literalString": "assets/travel_images/santorini_from_space.jpg" - }, - "fit": "cover" - } - } + "component": "Image", + "url": "assets/travel_images/santorini_from_space.jpg", + "fit": "cover" }, { "id": "greece_food_image", - "component": { - "Image": { - "fit": "cover", - "url": { - "literalString": "assets/travel_images/saffron_gatherers_fresco_santorini.jpg" - } - } - } + "component": "Image", + "fit": "cover", + "url": "assets/travel_images/saffron_gatherers_fresco_santorini.jpg" } ] '''; diff --git a/examples/travel_app/lib/src/config/configuration.dart b/examples/travel_app/lib/src/config/configuration.dart deleted file mode 100644 index 230831bb2..000000000 --- a/examples/travel_app/lib/src/config/configuration.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Enum for selecting which AI backend to use. -enum AiBackend { - /// Use Firebase AI - firebase, - - /// Use Google Generative AI - googleGenerativeAi, -} - -/// Configuration for which AI backend to use. -/// Change this value to switch between backends. -const AiBackend aiBackend = AiBackend.googleGenerativeAi; diff --git a/examples/travel_app/lib/src/fake_ai_client.dart b/examples/travel_app/lib/src/fake_ai_client.dart new file mode 100644 index 000000000..5d1aac4df --- /dev/null +++ b/examples/travel_app/lib/src/fake_ai_client.dart @@ -0,0 +1,57 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:genui/genui.dart'; + +import 'ai_client/ai_client.dart'; + +class FakeAiClient implements AiClient { + final _a2uiMessageController = StreamController.broadcast(); + final _textResponseController = StreamController.broadcast(); + final _errorController = StreamController.broadcast(); + + @override + Stream get a2uiMessageStream => _a2uiMessageController.stream; + @override + Stream get textResponseStream => _textResponseController.stream; + Stream get errorStream => _errorController.stream; + + int sendRequestCallCount = 0; + Completer? sendRequestCompleter; + + @override + Future sendRequest( + ChatMessage message, { + Iterable? history, + A2UiClientCapabilities? clientCapabilities, + Map? clientDataModel, + CancellationSignal? cancellationSignal, + }) async { + sendRequestCallCount++; + if (sendRequestCompleter != null) { + await sendRequestCompleter!.future; + } + } + + void addA2uiMessage(A2uiMessage message) { + _a2uiMessageController.add(message); + } + + void addTextResponse(String text) { + _textResponseController.add(text); + } + + void addError(Object error) { + _errorController.add(error); + } + + @override + void dispose() { + _a2uiMessageController.close(); + _textResponseController.close(); + _errorController.close(); + } +} diff --git a/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart b/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart index a703f6e2a..adec208ed 100644 --- a/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart +++ b/examples/travel_app/lib/src/tools/booking/list_hotels_tool.dart @@ -5,6 +5,8 @@ import 'package:genui/genui.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +import '../../ai_client/tools.dart'; + import 'model.dart'; /// An [AiTool] for listing hotels. @@ -28,10 +30,7 @@ class ListHotelsTool extends AiTool> { 'The check-out date in ISO 8601 format (YYYY-MM-DD).', format: 'date', ), - 'guests': S.integer( - description: 'The number of guests.', - minimum: 1, - ), + 'guests': S.integer(description: 'The number of guests.'), }, required: ['query', 'checkIn', 'checkOut', 'guests'], ), diff --git a/examples/travel_app/lib/src/travel_planner_page.dart b/examples/travel_app/lib/src/travel_planner_page.dart index e6ad0a8ae..2491aa18e 100644 --- a/examples/travel_app/lib/src/travel_planner_page.dart +++ b/examples/travel_app/lib/src/travel_planner_page.dart @@ -4,14 +4,15 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart'; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; +import 'package:genui/genui.dart' hide Conversation; +import 'package:genui/genui.dart' as genui; +import 'ai_client/ai_client.dart'; +import 'ai_client/google_generative_ai_client.dart'; import 'asset_images.dart'; import 'catalog.dart'; -import 'config/configuration.dart'; // Conditionally import non-web version so we can read from shell env vars in // non-web version. import 'config/io_get_api_key.dart' @@ -27,7 +28,7 @@ Future loadImagesJson() async { /// The main page for the travel planner application. /// /// This stateful widget manages the core user interface and application logic. -/// It initializes the [A2uiMessageProcessor] and [ContentGenerator], maintains +/// It initializes the [SurfaceController] and [A2uiTransportAdapter], maintains /// the conversation history, and handles the interaction between the user, the /// AI, and the dynamically generated UI. /// @@ -37,16 +38,16 @@ Future loadImagesJson() async { class TravelPlannerPage extends StatefulWidget { /// Creates a new [TravelPlannerPage]. /// - /// An optional [contentGenerator] can be provided, which is useful for + /// An optional [aiClient] can be provided, which is useful for /// testing or using a custom AI client implementation. If not provided, a - /// default [FirebaseAiContentGenerator] is created. - const TravelPlannerPage({this.contentGenerator, super.key}); + /// default [GoogleGenerativeAiClient] is created. + const TravelPlannerPage({this.aiClient, super.key}); /// The AI client to use for the application. /// - /// If null, a default instance of [FirebaseAiContentGenerator] will be - /// created within the page's state. - final ContentGenerator? contentGenerator; + /// If null, a default instance will be created. + /// This must be an instance of [AiClient]. + final AiClient? aiClient; @override State createState() => _TravelPlannerPageState(); @@ -54,8 +55,17 @@ class TravelPlannerPage extends StatefulWidget { class _TravelPlannerPageState extends State with AutomaticKeepAliveClientMixin { - late final GenUiConversation _uiConversation; - late final StreamSubscription _userMessageSubscription; + late final SurfaceController _processor; + late final genui.Conversation _uiConversation; + late final A2uiTransportAdapter _controller; + + final ValueNotifier> _messages = ValueNotifier([]); + final ValueNotifier _isProcessing = ValueNotifier(false); + String _currentStreamingText = ''; + + // We keep a reference to the client to dispose it if we created it. + AiClient? _client; + bool _didCreateClient = false; final _textController = TextEditingController(); final _scrollController = ScrollController(); @@ -63,62 +73,113 @@ class _TravelPlannerPageState extends State @override void initState() { super.initState(); - final a2uiMessageProcessor = A2uiMessageProcessor( - catalogs: [travelAppCatalog], - ); - _userMessageSubscription = a2uiMessageProcessor.onSubmit.listen( - _handleUserMessageFromUi, + // Wire up the controller's onSend to the appropriate client + _controller = A2uiTransportAdapter( + onSend: (message) async { + // Reset streaming text for new turn + _currentStreamingText = ''; + _messages.value = [..._messages.value, message]; + // Send request + await _sendRequest(_client!, message, _messages.value); + }, ); + _processor = SurfaceController(catalogs: [travelAppCatalog]); // Create the appropriate content generator based on configuration - final ContentGenerator contentGenerator = - widget.contentGenerator ?? - switch (aiBackend) { - AiBackend.googleGenerativeAi => () { - return GoogleGenerativeAiContentGenerator( - catalog: travelAppCatalog, - systemInstruction: prompt, - additionalTools: [ - ListHotelsTool( - onListHotels: BookingService.instance.listHotels, - ), - ], - apiKey: getApiKey(), - ); - }(), - AiBackend.firebase => FirebaseAiContentGenerator( - catalog: travelAppCatalog, - systemInstruction: prompt, - additionalTools: [ - ListHotelsTool(onListHotels: BookingService.instance.listHotels), + _client = widget.aiClient; + if (_client == null) { + _didCreateClient = true; + _client = GoogleGenerativeAiClient( + catalog: travelAppCatalog, + systemInstruction: prompt, + additionalTools: [ + ListHotelsTool(onListHotels: BookingService.instance.listHotels), + ], + apiKey: getApiKey(), + ); + } + + _wireClient(_client!, _controller); + + _uiConversation = genui.Conversation( + transport: _controller, + controller: _processor, + ); + + _uiConversation.state.addListener(() { + _isProcessing.value = _uiConversation.state.value.isWaiting; + }); + + _uiConversation.events.listen((event) { + if (event is ConversationContentReceived) { + if (event.text.isNotEmpty) { + _currentStreamingText += event.text; + + final updatedMessages = List.from(_messages.value); + if (updatedMessages.isNotEmpty && + updatedMessages.last.role == ChatMessageRole.model && + !updatedMessages.last.parts.any( + (p) => p is DataPart && p.isUiPart, + )) { + updatedMessages.removeLast(); + } + updatedMessages.add(ChatMessage.model(_currentStreamingText)); + _messages.value = updatedMessages; + + _scrollToBottom(); + } + } else if (event is ConversationSurfaceAdded) { + final updatedMessages = List.from(_messages.value); + updatedMessages.add( + ChatMessage( + role: ChatMessageRole.model, + parts: [ + UiPart.create( + definition: event.definition, + surfaceId: event.surfaceId, + ), ], ), - }; - - _uiConversation = GenUiConversation( - a2uiMessageProcessor: a2uiMessageProcessor, - contentGenerator: contentGenerator, - onSurfaceUpdated: (update) { + ); + _messages.value = updatedMessages; + // Reset streaming text so that any subsequent text is treated as a new + // message chunk after the UI component, rather than being appended to + // the previous text block (which would be confusing if the UI is in the + // middle). + _currentStreamingText = ''; _scrollToBottom(); - }, - onSurfaceAdded: (update) { + } else if (event is ConversationComponentsUpdated) { _scrollToBottom(); - }, - onTextResponse: (text) { - if (!mounted) return; - if (text.isNotEmpty) { - _scrollToBottom(); - } - }, - ); + } + }); } + void _wireClient(AiClient client, A2uiTransportAdapter controller) { + client.a2uiMessageStream.listen(controller.addMessage); + client.textResponseStream.listen(controller.addChunk); + } + + Future _sendRequest( + AiClient client, + ChatMessage message, + Iterable history, + ) { + return client.sendRequest(message, history: history); + } + + ValueListenable get isProcessing => _isProcessing; + @override void dispose() { - _userMessageSubscription.cancel(); + _processor.dispose(); _uiConversation.dispose(); + if (_didCreateClient) { + _client?.dispose(); + } _textController.dispose(); _scrollController.dispose(); + _messages.dispose(); + _isProcessing.dispose(); super.dispose(); } @@ -138,15 +199,11 @@ class _TravelPlannerPageState extends State await _uiConversation.sendRequest(message); } - void _handleUserMessageFromUi(ChatMessage message) { - _scrollToBottom(); - } - void _sendPrompt(String text) { - if (_uiConversation.isProcessing.value || text.trim().isEmpty) return; + if (_isProcessing.value || text.trim().isEmpty) return; _scrollToBottom(); _textController.clear(); - _triggerInference(UserMessage.text(text)); + _triggerInference(ChatMessage.user(text)); } @override @@ -160,11 +217,11 @@ class _TravelPlannerPageState extends State child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 1000), child: ValueListenableBuilder>( - valueListenable: _uiConversation.conversation, + valueListenable: _messages, builder: (context, messages, child) { return Conversation( messages: messages, - manager: _uiConversation.a2uiMessageProcessor, + manager: _processor, scrollController: _scrollController, ); }, @@ -174,7 +231,7 @@ class _TravelPlannerPageState extends State Padding( padding: const EdgeInsets.all(8.0), child: ValueListenableBuilder( - valueListenable: _uiConversation.isProcessing, + valueListenable: _isProcessing, builder: (context, isThinking, child) { return _ChatInput( controller: _textController, @@ -301,9 +358,9 @@ to the user. the itinerary, include all necessary `itineraryEntry` items for hotels and transport with generic details and a status of `choiceRequired`. - Note that during this step, the user may change their search parameters and - resubmit, in which case you should regenerate the itinerary to match their - desires, updating the existing surface. + During this step, the user may change their search parameters and resubmit, + in which case you should regenerate the itinerary to match their desires, + updating the existing surface. 4. Booking: Booking each part of the itinerary one step at a time. This involves booking every accommodation, transport and activity in the @@ -346,70 +403,23 @@ the user can return to the main booking flow once they have done some research. ## Controlling the UI -Use the provided tools to build and manage the user interface in response to the -user's requests. To display or update a UI, you must first call the -`surfaceUpdate` tool to define all the necessary components. After defining the -components, you must call the `beginRendering` tool to specify the root -component that should be displayed. - -- Adding surfaces: Most of the time, you should only add new surfaces to the - conversation. This is less confusing for the user, because they can easily - find this new content at the bottom of the conversation. -- Updating surfaces: You should update surfaces when you are running an - iterative search flow, e.g. the user is adjusting filter values and generating - an itinerary or a booking accommodation etc. This is less confusing for the - user because it avoids confusing the conversation with many versions of the - same itinerary etc. - -Once you add or update a surface and are waiting for user input, the -conversation turn is complete, and you should call the provideFinalOutput tool. - -If you are displaying more than one component, you should use a `Column` widget -as the root and add the other components as children. - -## UI style - -Always prefer to communicate using UI elements rather than text. Only respond -with text if you need to provide a short explanation of how you've updated the -UI. - -- TravelCarousel: Always make sure there are at least four options in the - carousel. If there are only 2 or 3 obvious options, just think of some - relevant alternatives that the user might be interested in. - -- Guiding the user: When the user has completed some action, e.g. they confirm - they want to book some accommodation or activity, always show a trailhead - suggesting what the user might want to do next (e.g. book the next detail in - the itinerary, repeat a search, research some related topic) so that they can - click rather than typing. - -- Itinerary Structure: Itineraries have a three-level structure. The root is - `itineraryWithDetails`, which provides an overview. Inside the modal view of - an `itineraryWithDetails`, you should use one or more `itineraryDay` widgets - to represent each day of the trip. Each `itineraryDay` should then contain a - list of `itineraryEntry` widgets, which represent specific activities, - bookings, or transport for that day. - -- Inputs: When you are asking for information from the user, you should always - include a submit button of some kind so that the user can indicate that they - are done providing information. Suggest initial values for number of people - and travel dates (e.g. 2 guests, dates of nearest weekend). The `InputGroup` - has a submit button, but if you are not using that, you can use an - `ElevatedButton`. Only use `OptionsFilterChipInput` widgets inside of a - `InputGroup`. **It is a strict requirement that all input chip widgets bind - their state to the data model. Under no circumstances should you use a literal - value for their state.** You should invent a suitable path in the data model - for each input. For example: `/search/destination`, - `/search/preferredActivities`, `/search/budget`. Specifically: - - - For `OptionsFilterChipInput`, `DateInputChip`, and `TextInputChip`, the - `value` parameter MUST be bound to the data model using a `path`. - - For `CheckboxFilterChipsInput`, the `selectedOptions` parameter MUST be - bound to the data model using a `path`. - -- State management: Try to maintain state by being aware of the user's - selections and preferences and setting them in the initial value fields of - input elements when updating surfaces or generating new ones. +You can control the UI by outputting valid A2UI JSON messages wrapped in markdown code blocks. +Supported messages are: `createSurface` and `updateComponents`. + +To show a new UI: +1. Output a `createSurface` message to define the surface ID and catalog. +2. Output an `updateComponents` message to populate the surface with components. + +To update an existing UI (e.g. adding items to an itinerary): +1. Output an `updateComponents` message with the existing `surfaceId` and the new component definitions. + +Properties: +- `createSurface`: requires `surfaceId`, `catalogId` (use the catalog ID provided in system instructions), and `sendDataModel: true`. +- `updateComponents`: requires `surfaceId` and a list of `components`. One component MUST have `id: "root"`. + +IMPORTANT: +- Do not use tools or function calls for UI generation. Use JSON text blocks. +- Ensure all JSON is valid and fenced with ```json ... ```. ## Images @@ -426,110 +436,81 @@ ${_imagesJson ?? ''} ## Example -Here is an example of the arguments to the `surfaceUpdate` tool. Note that the -`root` widget ID must be present in the `widgets` list, and it should contain -the other widgets. +Here is an example of creating a trip planner UI. + +```json +{ + "createSurface": { + "surfaceId": "mexico_trip_planner", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", + "sendDataModel": true + } +} +``` ```json { - "surfaceId": "mexico_trip_planner", - "definition": { - "root": "root_column", - "widgets": [ + "updateComponents": { + "surfaceId": "mexico_trip_planner", + "components": [ { - "id": "root_column", - "widget": { - "Column": { - "children": ["trip_title", "itinerary"] - } - } + "id": "root", + "component": "Column", + "children": ["trip_title", "itinerary"] }, { "id": "trip_title", - "widget": { - "Text": { - "text": "Trip to Mexico City" - } - } + "component": "Text", + "text": "Trip to Mexico City", + "variant": "h2" }, { "id": "itinerary", - "widget": { - "ItineraryWithDetails": { - "title": "Mexico City Adventure", - "subheading": "3-day Itinerary", - "imageChildId": "mexico_city_image", - "child": "itinerary_details" - } - } + "component": "ItineraryWithDetails", + "title": "Mexico City Adventure", + "subheading": "3-day Itinerary", + "imageChildId": "mexico_city_image", + "child": "itinerary_details" }, { "id": "mexico_city_image", - "widget": { - "Image": { - "location": "assets/travel_images/mexico_city.jpg" - } - } + "component": "Image", + "url": "assets/travel_images/mexico_city.jpg", + "variant": "mediumFeature" }, { "id": "itinerary_details", - "widget": { - "Column": { - "children": ["day1"] - } - } + "component": "Column", + "children": ["day1"] }, { "id": "day1", - "widget": { - "ItineraryDay": { - "title": "Day 1", - "subtitle": "Arrival and Exploration", - "description": "Your first day in Mexico City will be focused on settling in and exploring the historic center.", - "imageChildId": "day1_image", - "children": ["day1_entry1", "day1_entry2"] - } - } + "component": "ItineraryDay", + "title": "Day 1", + "subtitle": "Arrival and Exploration", + "description": "Your first day in Mexico City...", + "imageChildId": "day1_image", + "children": ["day1_entry1"] }, { "id": "day1_image", - "widget": { - "Image": { - "location": "assets/travel_images/mexico_city.jpg" - } - } + "component": "Image", + "url": "assets/travel_images/mexico_city.jpg", + "variant": "mediumFeature" }, { "id": "day1_entry1", - "widget": { - "ItineraryEntry": { - "type": "transport", - "title": "Arrival at MEX Airport", - "time": "2:00 PM", - "bodyText": "Arrive at Mexico City International Airport (MEX), clear customs, and pick up your luggage.", - "status": "noBookingRequired" - } - } - }, - { - "id": "day1_entry2", - "widget": { - "ItineraryEntry": { - "type": "activity", - "title": "Explore the Zocalo", - "subtitle": "Historic Center", - "time": "4:00 PM - 6:00 PM", - "address": "Plaza de la Constitución S/N, Centro Histórico, Ciudad de México", - "bodyText": "Head to the Zocalo, the main square of Mexico City. Visit the Metropolitan Cathedral and the National Palace.", - "status": "noBookingRequired" - } - } + "component": "ItineraryEntry", + "type": "transport", + "title": "Arrival at MEX Airport", + "time": "2:00 PM", + "bodyText": "Arrive at Mexico City...", + "status": "noBookingRequired" } ] } } ``` -When updating or showing UIs, **ALWAYS** use the surfaceUpdate tool to supply -them. Prefer to collect and show information by creating a UI for it. +When updating or showing UIs, **ALWAYS** use the JSON messages as described above. Prefer to collect and show information by creating a UI for it. '''; diff --git a/examples/travel_app/lib/src/widgets/conversation.dart b/examples/travel_app/lib/src/widgets/conversation.dart index a3539d542..c3ac8157f 100644 --- a/examples/travel_app/lib/src/widgets/conversation.dart +++ b/examples/travel_app/lib/src/widgets/conversation.dart @@ -7,10 +7,10 @@ import 'package:flutter/material.dart'; import 'package:genui/genui.dart'; typedef UserPromptBuilder = - Widget Function(BuildContext context, UserMessage message); + Widget Function(BuildContext context, ChatMessage message); typedef UserUiInteractionBuilder = - Widget Function(BuildContext context, UserUiInteractionMessage message); + Widget Function(BuildContext context, ChatMessage message); class Conversation extends StatelessWidget { const Conversation({ @@ -24,7 +24,7 @@ class Conversation extends StatelessWidget { }); final List messages; - final A2uiMessageProcessor manager; + final SurfaceHost manager; final UserPromptBuilder? userPromptBuilder; final UserUiInteractionBuilder? userUiInteractionBuilder; final bool showInternalMessages; @@ -36,26 +36,65 @@ class Conversation extends StatelessWidget { if (showInternalMessages) { return true; } - return message is! InternalMessage && message is! ToolResponseMessage; + final isInternal = message.role == ChatMessageRole.system; + final bool isTool = message.parts.any( + (p) => p is ToolPart && p.result != null, + ); + return !isInternal && !isTool; }).toList(); return ListView.builder( controller: scrollController, itemCount: renderedMessages.length, itemBuilder: (context, index) { final ChatMessage message = renderedMessages[index]; - switch (message) { - case UserMessage(): - return userPromptBuilder != null - ? userPromptBuilder!(context, message) - : ChatMessageView( - text: message.parts - .whereType() - .map((part) => part.text) - .join('\n'), - icon: Icons.person, - alignment: MainAxisAlignment.end, - ); - case AiTextMessage(): + switch (message.role) { + case ChatMessageRole.user: + final bool hasUiInteraction = message.parts.any( + (p) => p.isUiInteractionPart, + ); + final String text = message.parts + .whereType() + .map((part) => part.text) + .join('\n'); + + if (text.isNotEmpty) { + return userPromptBuilder != null + ? userPromptBuilder!(context, message) + : ChatMessageView( + text: text, + icon: Icons.person, + alignment: MainAxisAlignment.end, + ); + } + if (message.parts.any((p) => p is ToolPart)) { + return InternalMessageView(content: message.parts.toString()); + } + + if (hasUiInteraction) { + return userUiInteractionBuilder != null + ? userUiInteractionBuilder!(context, message) + : const SizedBox.shrink(); + } + + return const SizedBox.shrink(); + + case ChatMessageRole.model: + final Iterable uiParts = message.parts + .whereType() + .where((p) => p.isUiPart); + if (uiParts.isNotEmpty) { + final UiPart uiPart = uiParts.first.asUiPart!; + return Padding( + padding: const EdgeInsets.all(16.0), + child: Surface( + key: ValueKey(uiPart.definition.surfaceId), + surfaceContext: manager.contextFor( + uiPart.definition.surfaceId, + ), + ), + ); + } + final String text = message.parts .whereType() .map((part) => part.text) @@ -68,23 +107,13 @@ class Conversation extends StatelessWidget { icon: Icons.smart_toy_outlined, alignment: MainAxisAlignment.start, ); - case AiUiMessage(): - return Padding( - padding: const EdgeInsets.all(16.0), - child: GenUiSurface( - key: message.uiKey, - host: manager, - surfaceId: message.surfaceId, - ), + + case ChatMessageRole.system: + return InternalMessageView( + content: message.parts + .map((p) => p is TextPart ? p.text : p.toString()) + .join('\n'), ); - case InternalMessage(): - return InternalMessageView(content: message.text); - case UserUiInteractionMessage(): - return userUiInteractionBuilder != null - ? userUiInteractionBuilder!(context, message) - : const SizedBox.shrink(); - case ToolResponseMessage(): - return InternalMessageView(content: message.results.toString()); } }, ); diff --git a/examples/travel_app/linux/flutter/generated_plugin_registrant.cc b/examples/travel_app/linux/flutter/generated_plugin_registrant.cc index e71a16d23..f6f23bfe9 100644 --- a/examples/travel_app/linux/flutter/generated_plugin_registrant.cc +++ b/examples/travel_app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/examples/travel_app/linux/flutter/generated_plugins.cmake b/examples/travel_app/linux/flutter/generated_plugins.cmake index 2e1de87a7..f16b4c342 100644 --- a/examples/travel_app/linux/flutter/generated_plugins.cmake +++ b/examples/travel_app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/travel_app/macos/.gitignore b/examples/travel_app/macos/.gitignore index 8effa017e..746adbb6b 100644 --- a/examples/travel_app/macos/.gitignore +++ b/examples/travel_app/macos/.gitignore @@ -1,7 +1,5 @@ # Flutter-related **/Flutter/ephemeral/ -**/Podfile -**/Podfile.lock **/Pods/ # Xcode-related diff --git a/examples/travel_app/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/travel_app/macos/Flutter/GeneratedPluginRegistrant.swift index c6c180db8..4dc2c0c72 100644 --- a/examples/travel_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/travel_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,11 +6,11 @@ import FlutterMacOS import Foundation import firebase_app_check -import firebase_auth import firebase_core +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/examples/travel_app/macos/Podfile b/examples/travel_app/macos/Podfile new file mode 100644 index 000000000..ff5ddb3b8 --- /dev/null +++ b/examples/travel_app/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/examples/travel_app/macos/Runner.xcodeproj/project.pbxproj b/examples/travel_app/macos/Runner.xcodeproj/project.pbxproj index d95b8795b..469f2e204 100644 --- a/examples/travel_app/macos/Runner.xcodeproj/project.pbxproj +++ b/examples/travel_app/macos/Runner.xcodeproj/project.pbxproj @@ -21,15 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 14D3C6AA03A2F53BEAA1E023 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F41263C2CBE0A340BA75F803 /* GoogleService-Info.plist */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 63482C6A6C08BB9F81EB622A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5B76D750F31DB184023289C /* Pods_Runner.framework */; }; - 92C895485B5D6EAE40BD982F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2902378D76640F9873A8E7BA /* Pods_RunnerTests.framework */; }; + 47729BB214424522F4652A19 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69CA789B79AA069E0375882 /* Pods_Runner.framework */; }; + 898B03D55D13823F90F6F36E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA4452312D426F9CC43213FF /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -63,12 +62,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2902378D76640F9873A8E7BA /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 175311DAA6BF4E8B51F3C004 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* genui_client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = genui_client.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* travel_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = travel_app.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -80,16 +79,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 62E0C56DB98B59A3F7E73136 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 6AFBC26FB7E6F79D9FF1ABBA /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 75E4CFAB98E85B3E246F3185 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 774839EDC278525C3B970ACB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 42E769C4FF5E827F1032467F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 4A1C4B9A3B97D992DC0D6275 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 7440398B1A53433B0A68060A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 8401E357C5F002F289CB4C77 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 83F8E3F6B06B1386EC732887 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 9D1ECB575545C70446AAF02C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - B5B76D750F31DB184023289C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F41263C2CBE0A340BA75F803 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; + B69CA789B79AA069E0375882 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D6570A15E980EA3B0C6E116E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + DA4452312D426F9CC43213FF /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 92C895485B5D6EAE40BD982F /* Pods_RunnerTests.framework in Frameworks */, + 898B03D55D13823F90F6F36E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -105,7 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 63482C6A6C08BB9F81EB622A /* Pods_Runner.framework in Frameworks */, + 47729BB214424522F4652A19 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -139,15 +137,14 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - EC60867614928B307CB4BFFC /* Pods */, - F41263C2CBE0A340BA75F803 /* GoogleService-Info.plist */, + 3C82DADEAAA3430E44A11B81 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* genui_client.app */, + 33CC10ED2044A3C60003C045 /* travel_app.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -188,27 +185,27 @@ path = Runner; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { + 3C82DADEAAA3430E44A11B81 /* Pods */ = { isa = PBXGroup; children = ( - B5B76D750F31DB184023289C /* Pods_Runner.framework */, - 2902378D76640F9873A8E7BA /* Pods_RunnerTests.framework */, + 7440398B1A53433B0A68060A /* Pods-Runner.debug.xcconfig */, + 4A1C4B9A3B97D992DC0D6275 /* Pods-Runner.release.xcconfig */, + D6570A15E980EA3B0C6E116E /* Pods-Runner.profile.xcconfig */, + 42E769C4FF5E827F1032467F /* Pods-RunnerTests.debug.xcconfig */, + 175311DAA6BF4E8B51F3C004 /* Pods-RunnerTests.release.xcconfig */, + 83F8E3F6B06B1386EC732887 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Frameworks; + name = Pods; + path = Pods; sourceTree = ""; }; - EC60867614928B307CB4BFFC /* Pods */ = { + D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 75E4CFAB98E85B3E246F3185 /* Pods-Runner.debug.xcconfig */, - 8401E357C5F002F289CB4C77 /* Pods-Runner.release.xcconfig */, - 774839EDC278525C3B970ACB /* Pods-Runner.profile.xcconfig */, - 62E0C56DB98B59A3F7E73136 /* Pods-RunnerTests.debug.xcconfig */, - 6AFBC26FB7E6F79D9FF1ABBA /* Pods-RunnerTests.release.xcconfig */, - 9D1ECB575545C70446AAF02C /* Pods-RunnerTests.profile.xcconfig */, + B69CA789B79AA069E0375882 /* Pods_Runner.framework */, + DA4452312D426F9CC43213FF /* Pods_RunnerTests.framework */, ); - name = Pods; - path = Pods; + name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ @@ -218,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 3C4AA3BB3F3CD21560A46BB8 /* [CP] Check Pods Manifest.lock */, + B43DFBEB34AA33AE342F3EA6 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -237,13 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 703477750A6EA69C0FBAFEB2 /* [CP] Check Pods Manifest.lock */, + 20BDEBCEFAF8AB293D5F829F /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 997FC1770EA917D1C5764C28 /* [CP] Embed Pods Frameworks */, + C18573173B478B8332B7FCD4 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -252,7 +249,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* genui_client.app */; + productReference = 33CC10ED2044A3C60003C045 /* travel_app.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -320,74 +317,73 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 14D3C6AA03A2F53BEAA1E023 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { + 20BDEBCEFAF8AB293D5F829F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { + 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( - Flutter/ephemeral/tripwire, ); outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; - 3C4AA3BB3F3CD21560A46BB8 /* [CP] Check Pods Manifest.lock */ = { + 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + Flutter/ephemeral/tripwire, ); - name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 703477750A6EA69C0FBAFEB2 /* [CP] Check Pods Manifest.lock */ = { + B43DFBEB34AA33AE342F3EA6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -402,14 +398,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 997FC1770EA917D1C5764C28 /* [CP] Embed Pods Frameworks */ = { + C18573173B478B8332B7FCD4 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -477,46 +473,46 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 62E0C56DB98B59A3F7E73136 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 42E769C4FF5E827F1032467F /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.genuiClient.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.travelApp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/genui_client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/genui_client"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/travel_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/travel_app"; }; name = Debug; }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 6AFBC26FB7E6F79D9FF1ABBA /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 175311DAA6BF4E8B51F3C004 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.genuiClient.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.travelApp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/genui_client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/genui_client"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/travel_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/travel_app"; }; name = Release; }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9D1ECB575545C70446AAF02C /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 83F8E3F6B06B1386EC732887 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.genuiClient.RunnerTests; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.travelApp.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/genui_client.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/genui_client"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/travel_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/travel_app"; }; name = Profile; }; diff --git a/examples/travel_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/travel_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b90d0dbaa..af141e5f7 100644 --- a/examples/travel_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/examples/travel_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -66,7 +66,7 @@ @@ -83,7 +83,7 @@ diff --git a/examples/travel_app/macos/Runner/Configs/AppInfo.xcconfig b/examples/travel_app/macos/Runner/Configs/AppInfo.xcconfig index c3c2aacec..0ff973443 100644 --- a/examples/travel_app/macos/Runner/Configs/AppInfo.xcconfig +++ b/examples/travel_app/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = genui_client +PRODUCT_NAME = travel_app // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.genuiClient +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.genui.travelApp // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 dev.flutter.genui. +PRODUCT_COPYRIGHT = Copyright © 2026 dev.flutter.genui. All rights reserved. diff --git a/examples/travel_app/pubspec.yaml b/examples/travel_app/pubspec.yaml index 4c893fc90..11ee2d387 100644 --- a/examples/travel_app/pubspec.yaml +++ b/examples/travel_app/pubspec.yaml @@ -19,8 +19,11 @@ dependencies: flutter: sdk: flutter genui: ^0.7.0 - genui_firebase_ai: ^0.7.0 - genui_google_generative_ai: ^0.7.0 + google_cloud_ai_generativelanguage_v1beta: ^0.4.0 + google_cloud_protobuf: ^0.4.0 + google_cloud_rpc: ^0.4.0 + google_cloud_type: ^0.4.0 + gpt_markdown: ^1.1.4 intl: ^0.20.2 json_schema_builder: ^0.1.3 diff --git a/examples/travel_app/test/ai_client/google_content_converter_test.dart b/examples/travel_app/test/ai_client/google_content_converter_test.dart new file mode 100644 index 000000000..8e891bfdb --- /dev/null +++ b/examples/travel_app/test/ai_client/google_content_converter_test.dart @@ -0,0 +1,70 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' + as google_ai; +import 'package:travel_app/src/ai_client/google_content_converter.dart'; + +void main() { + group('GoogleContentConverter', () { + test('converts interaction json to text', () { + final converter = GoogleContentConverter(); + final interactionData = {'foo': 'bar'}; + final Uint8List bytes = utf8.encode(jsonEncode(interactionData)); + + final message = ChatMessage( + role: ChatMessageRole.user, + parts: [ + DataPart( + Uint8List.fromList(bytes), + mimeType: 'application/vnd.genui.interaction+json', + ), + ], + ); + + final List convertResult = converter.toGoogleAiContent( + [message], + ); + + expect(convertResult, hasLength(1)); + final google_ai.Content content = convertResult.first; + expect(content.role, 'user'); + expect(content.parts, hasLength(1)); + + final google_ai.Part part = content.parts.first; + expect(part.text, jsonEncode(interactionData)); + expect(part.inlineData, isNull); + }); + + test('converts other mime types to blobs', () { + final converter = GoogleContentConverter(); + final bytes = Uint8List.fromList([1, 2, 3]); + + final message = ChatMessage( + role: ChatMessageRole.user, + parts: [DataPart(bytes, mimeType: 'image/png')], + ); + + final List convertResult = converter.toGoogleAiContent( + [message], + ); + + expect(convertResult, hasLength(1)); + final google_ai.Content content = convertResult.first; + expect(content.role, 'user'); + expect(content.parts, hasLength(1)); + + final google_ai.Part part = content.parts.first; + expect(part.text, isNull); + expect(part.inlineData, isNotNull); + expect(part.inlineData!.mimeType, 'image/png'); + expect(part.inlineData!.data, bytes); + }); + }); +} diff --git a/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart b/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart new file mode 100644 index 000000000..214c7bfb3 --- /dev/null +++ b/examples/travel_app/test/ai_client/google_generative_ai_client_test.dart @@ -0,0 +1,84 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' + as google_ai; +import 'package:travel_app/src/ai_client/google_generative_ai_client.dart'; +import 'package:travel_app/src/ai_client/google_generative_service_interface.dart'; + +void main() { + group('GoogleGenerativeAiClient', () { + late FakeGoogleGenerativeService fakeService; + late GoogleGenerativeAiClient client; + + setUp(() { + fakeService = FakeGoogleGenerativeService(); + client = GoogleGenerativeAiClient( + catalog: const Catalog({}), // Empty catalog for testing + apiKey: 'test-api-key', + serviceFactory: ({required configuration}) => fakeService, + ); + }); + + test('sendRequest includes clientDataModel in prompt', () async { + final Map clientData = {'theme': 'dark', 'userId': 123}; + final message = ChatMessage( + role: ChatMessageRole.user, + parts: [const TextPart('Hello')], + ); + + // Stub the response to avoid null errors + fakeService.responseToReturn = google_ai.GenerateContentResponse( + candidates: [ + google_ai.Candidate( + content: google_ai.Content( + parts: [google_ai.Part(text: 'Response')], + ), + ), + ], + ); + + await client.sendRequest(message, clientDataModel: clientData); + + final google_ai.GenerateContentRequest? capturedRequest = + fakeService.capturedRequest; + expect(capturedRequest, isNotNull); + + // Verify that the clientDataModel is included in the request contents + var foundClientData = false; + for (final google_ai.Content content in capturedRequest!.contents) { + for (final google_ai.Part part in content.parts) { + if (part.text != null && part.text!.contains('Client Data Model:')) { + expect(part.text, contains('"theme": "dark"')); + expect(part.text, contains('"userId": 123')); + foundClientData = true; + } + } + } + expect( + foundClientData, + isTrue, + reason: 'Client Data Model not found in prompt', + ); + }); + }); +} + +class FakeGoogleGenerativeService implements GoogleGenerativeServiceInterface { + google_ai.GenerateContentRequest? capturedRequest; + google_ai.GenerateContentResponse? responseToReturn; + + @override + Future generateContent( + google_ai.GenerateContentRequest request, + ) async { + capturedRequest = request; + return responseToReturn ?? google_ai.GenerateContentResponse(); + } + + @override + void close() {} +} diff --git a/examples/travel_app/test/catalog_validation_test.dart b/examples/travel_app/test/catalog_validation_test.dart index 17710effb..3569a3621 100644 --- a/examples/travel_app/test/catalog_validation_test.dart +++ b/examples/travel_app/test/catalog_validation_test.dart @@ -9,9 +9,16 @@ import 'package:travel_app/src/catalog.dart'; void main() { group('Travel App Catalog Validation', () { + final Set existingNames = travelAppCatalog.items + .map((i) => i.name) + .toSet(); + final List coreItemsToAdd = BasicCatalogItems.asCatalog().items + .where((i) => !existingNames.contains(i.name)) + .toList(); + final mergedCatalog = Catalog([ ...travelAppCatalog.items, - ...CoreCatalogItems.asCatalog().items, + ...coreItemsToAdd, ]); for (final CatalogItem item in travelAppCatalog.items) { diff --git a/examples/travel_app/test/checkbox_filter_chips_input_test.dart b/examples/travel_app/test/checkbox_filter_chips_input_test.dart index c11ab7571..d51281476 100644 --- a/examples/travel_app/test/checkbox_filter_chips_input_test.dart +++ b/examples/travel_app/test/checkbox_filter_chips_input_test.dart @@ -19,15 +19,15 @@ void main() { return Center( child: checkboxFilterChipsInput.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: { 'chipLabel': 'Amenities', 'options': ['Wifi', 'Pool', 'Gym'], - 'selectedOptions': { - 'literalArray': ['Wifi', 'Gym'], - }, + 'selectedOptions': ['Wifi', 'Gym'], 'iconName': 'hotel', }, id: 'test', + type: 'CheckboxFilterChipsInput', buildChild: (_, [_]) => const SizedBox(), dispatchEvent: (_) {}, buildContext: context, @@ -45,4 +45,59 @@ void main() { expect(find.text('Wifi, Gym'), findsOneWidget); }); + + testWidgets( + 'CheckboxFilterChipsInput updates DataContext with implicit binding', + (WidgetTester tester) async { + final dataModel = DataModel(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return Center( + child: checkboxFilterChipsInput.widgetBuilder( + CatalogItemContext( + getCatalogItem: (type) => null, + data: { + 'chipLabel': 'Amenities', + 'options': ['Wifi', 'Pool', 'Gym'], + 'selectedOptions': ['Wifi'], + 'iconName': 'hotel', + }, + id: 'test', + type: 'CheckboxFilterChipsInput', + buildChild: (_, [_]) => const SizedBox(), + dispatchEvent: (_) {}, + buildContext: context, + dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, + surfaceId: 'surface1', + ), + ), + ); + }, + ), + ), + ), + ); + + // Verify initial state + expect(find.text('Wifi'), findsOneWidget); + expect(dataModel.getValue(DataPath('test.value')), isNull); + + // Open the modal + await tester.tap(find.byType(FilterChip)); + await tester.pumpAndSettle(); + + // Select 'Pool' + await tester.tap(find.text('Pool')); + await tester.pump(); + + // Update happens immediately on selection change in the modal + final value = + dataModel.getValue(DataPath('test.value')) as List?; + expect(value, containsAll(['Wifi', 'Pool'])); + }, + ); } diff --git a/examples/travel_app/test/date_input_chip_test.dart b/examples/travel_app/test/date_input_chip_test.dart index e76cca820..bf8c5b2fe 100644 --- a/examples/travel_app/test/date_input_chip_test.dart +++ b/examples/travel_app/test/date_input_chip_test.dart @@ -20,11 +20,10 @@ void main() { builder: (context) { return dateInputChip.widgetBuilder( CatalogItemContext( - data: { - 'value': {'literalString': '2025-09-20'}, - 'label': 'Test Date', - }, + getCatalogItem: (type) => null, + data: {'value': '2025-09-20', 'label': 'Test Date'}, id: 'test_chip', + type: 'DateInputChip', buildChild: (data, [_]) => const SizedBox(), dispatchEvent: (event) {}, buildContext: context, @@ -55,11 +54,13 @@ void main() { builder: (context) { return dateInputChip.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: { 'value': {'path': '/testDate'}, 'label': 'Test Date', }, id: 'test_chip', + type: 'DateInputChip', buildChild: (data, [_]) => const SizedBox(), dispatchEvent: (event) {}, buildContext: context, @@ -95,11 +96,13 @@ void main() { builder: (context) { return dateInputChip.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: { 'value': {'path': '/testDate'}, 'label': 'Test Date', }, id: 'test_chip', + type: 'DateInputChip', buildChild: (data, [_]) => const SizedBox(), dispatchEvent: (event) {}, buildContext: context, @@ -136,11 +139,13 @@ void main() { builder: (context) { return dateInputChip.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: { 'value': {'path': '/testDate'}, 'label': 'Test Date', }, id: 'test_chip', + type: 'DateInputChip', buildChild: (data, [_]) => const SizedBox(), dispatchEvent: (event) {}, buildContext: context, @@ -179,4 +184,51 @@ void main() { findsOneWidget, ); }); + testWidgets( + 'DateInputChip updates implicit data model path on date selection when ' + 'initialized with literal', + (WidgetTester tester) async { + final dataModel = DataModel(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return dateInputChip.widgetBuilder( + CatalogItemContext( + getCatalogItem: (type) => null, + data: {'value': '2025-09-20', 'label': 'Test Date'}, + id: 'test_chip_implicit', + type: 'DateInputChip', + buildChild: (data, [_]) => const SizedBox(), + dispatchEvent: (event) {}, + buildContext: context, + dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, + surfaceId: 'surface1', + ), + ); + }, + ), + ), + ), + ); + + expect(find.text('Test Date: Sep 20, 2025'), findsOneWidget); + + await tester.tap(find.byType(FilterChip)); + await tester.pumpAndSettle(); + + await tester.tap(find.text('10')); + await tester.pumpAndSettle(); + + // Verify update to implicit path: test_chip_implicit.value + expect( + dataModel.getValue(DataPath('test_chip_implicit.value')), + '2025-09-10', + ); + expect(find.text('Test Date: Sep 10, 2025'), findsOneWidget); + }, + ); } diff --git a/examples/travel_app/test/image_catalog_test.dart b/examples/travel_app/test/image_catalog_test.dart index 179dd510b..905b3d5a5 100644 --- a/examples/travel_app/test/image_catalog_test.dart +++ b/examples/travel_app/test/image_catalog_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: avoid_dynamic_calls - import 'dart:convert'; import 'dart:io'; @@ -18,7 +16,7 @@ void main() { final String imageAssets = await assetImageCatalogJson(); final List imageList = (jsonDecode(imageAssets) as List) - .map((e) => e['image_file_name'] as String) + .map((e) => (e as Map).values.first as String) .toList(); final imageDir = Directory(assetImageCatalogPath); diff --git a/examples/travel_app/test/input_group_test.dart b/examples/travel_app/test/input_group_test.dart index 601b2b42b..2d4a1b141 100644 --- a/examples/travel_app/test/input_group_test.dart +++ b/examples/travel_app/test/input_group_test.dart @@ -13,9 +13,11 @@ void main() { 'renders children and dispatches submit event on button press', (WidgetTester tester) async { final Map data = { - 'submitLabel': {'literalString': 'Submit'}, + 'submitLabel': 'Submit', 'children': ['child1', 'child2'], - 'action': {'name': 'submitAction'}, + 'action': { + 'event': {'name': 'submitAction'}, + }, }; UiEvent? dispatchedEvent; @@ -28,8 +30,10 @@ void main() { builder: (context) { return inputGroup.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'InputGroup', buildChild: buildChild, dispatchEvent: (event) { dispatchedEvent = event; @@ -65,9 +69,11 @@ void main() { WidgetTester tester, ) async { final Map data = { - 'submitLabel': {'literalString': 'Submit'}, + 'submitLabel': 'Submit', 'children': [], - 'action': {'name': 'submitAction'}, + 'action': { + 'event': {'name': 'submitAction'}, + }, }; await tester.pumpWidget( @@ -77,8 +83,10 @@ void main() { builder: (context) { return inputGroup.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'InputGroup', buildChild: (_, [_]) => const SizedBox.shrink(), dispatchEvent: (UiEvent _) {}, buildContext: context, diff --git a/examples/travel_app/test/itinerary_test.dart b/examples/travel_app/test/itinerary_test.dart index 1fb08512d..7f3c7d21e 100644 --- a/examples/travel_app/test/itinerary_test.dart +++ b/examples/travel_app/test/itinerary_test.dart @@ -20,25 +20,24 @@ void main() { } final Map testData = { - 'title': {'literalString': 'My Awesome Trip'}, - 'subheading': {'literalString': 'A 3-day adventure'}, + 'title': 'My Awesome Trip', + 'subheading': 'A 3-day adventure', 'imageChildId': 'image1', 'days': [ { - 'title': {'literalString': 'Day 1'}, - 'subtitle': {'literalString': 'Arrival and Exploration'}, - 'description': {'literalString': 'Welcome to the city!'}, + 'title': 'Day 1', + 'subtitle': 'Arrival and Exploration', + 'description': 'Welcome to the city!', 'imageChildId': 'image2', 'entries': [ { - 'title': {'literalString': 'Choose your hotel'}, - 'bodyText': {'literalString': 'Select a hotel for your stay.'}, - 'time': {'literalString': '3:00 PM'}, + 'title': 'Choose your hotel', + 'bodyText': 'Select a hotel for your stay.', + 'time': '3:00 PM', 'type': 'accommodation', 'status': 'choiceRequired', 'choiceRequiredAction': { - 'name': 'testAction', - 'context': [], + 'event': {'name': 'testAction', 'context': {}}, }, }, ], @@ -46,23 +45,29 @@ void main() { ], }; - final Widget itineraryWidget = itinerary.widgetBuilder( - CatalogItemContext( - data: testData, - id: 'itinerary1', - buildChild: (data, [_]) => SizedBox(key: Key(data)), - dispatchEvent: mockDispatchEvent, - buildContext: tester.element(find.byType(Container)), - dataContext: DataContext(DataModel(), '/'), - getComponent: (String componentId) => null, - surfaceId: 'surface1', - ), - ); - - // 2. Pump the widget + // 2. Pump the widget using Builder to get a valid context await tester.pumpWidget( MaterialApp( - home: Scaffold(body: Center(child: itineraryWidget)), + home: Builder( + builder: (BuildContext context) { + final Widget itineraryWidget = itinerary.widgetBuilder( + CatalogItemContext( + getCatalogItem: (type) => null, + data: testData, + id: 'itinerary1', + type: 'Itinerary', + buildChild: (data, [_]) => SizedBox(key: Key(data)), + dispatchEvent: mockDispatchEvent, + buildContext: context, + dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => + throw UnimplementedError(), + surfaceId: 'surface1', + ), + ); + return Scaffold(body: Center(child: itineraryWidget)); + }, + ), ), ); @@ -80,6 +85,7 @@ void main() { expect(find.byType(FilledButton), findsOneWidget); // 6. Simulate tap on the action button + // Find button by text "Choose" inside FilledButton await tester.tap(find.widgetWithText(FilledButton, 'Choose')); await tester.pumpAndSettle(); diff --git a/examples/travel_app/test/main_test.dart b/examples/travel_app/test/main_test.dart index f9cb74bc3..74cf8e351 100644 --- a/examples/travel_app/test/main_test.dart +++ b/examples/travel_app/test/main_test.dart @@ -6,33 +6,29 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/test/fake_content_generator.dart'; import 'package:travel_app/main.dart' as app; +import 'package:travel_app/src/fake_ai_client.dart'; void main() { testWidgets('Can send a prompt', (WidgetTester tester) async { - final mockContentGenerator = FakeContentGenerator(); - await tester.pumpWidget( - app.TravelApp(contentGenerator: mockContentGenerator), - ); + final mockClient = FakeAiClient(); + await tester.pumpWidget(app.TravelApp(aiClient: mockClient)); await tester.enterText(find.byType(TextField), 'test prompt'); await tester.testTextInput.receiveAction(TextInputAction.send); - mockContentGenerator.addTextResponse('AI response'); + mockClient.addTextResponse('AI response'); await tester.pumpAndSettle(); - expect(mockContentGenerator.sendRequestCallCount, 1); + expect(mockClient.sendRequestCallCount, 1); expect(find.text('test prompt'), findsOneWidget); expect(find.text('AI response'), findsOneWidget); }); testWidgets('Shows spinner while thinking', (WidgetTester tester) async { - final mockContentGenerator = FakeContentGenerator(); + final mockClient = FakeAiClient(); final completer = Completer(); - mockContentGenerator.sendRequestCompleter = completer; - await tester.pumpWidget( - app.TravelApp(contentGenerator: mockContentGenerator), - ); + mockClient.sendRequestCompleter = completer; + await tester.pumpWidget(app.TravelApp(aiClient: mockClient)); await tester.enterText(find.byType(TextField), 'test prompt'); await tester.testTextInput.receiveAction(TextInputAction.send); @@ -46,7 +42,7 @@ void main() { // Complete the response. completer.complete(); - mockContentGenerator.addTextResponse('AI response'); + mockClient.addTextResponse('AI response'); await tester.pumpAndSettle(); // The spinner should be gone. diff --git a/examples/travel_app/test/options_filter_chip_input_test.dart b/examples/travel_app/test/options_filter_chip_input_test.dart index d28905e54..1dc5333a4 100644 --- a/examples/travel_app/test/options_filter_chip_input_test.dart +++ b/examples/travel_app/test/options_filter_chip_input_test.dart @@ -27,8 +27,10 @@ void main() { builder: (context) { return optionsFilterChipInput.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'OptionsFilterChipInput', buildChild: (_, [_]) => const SizedBox.shrink(), dispatchEvent: (event) {}, buildContext: context, @@ -89,8 +91,10 @@ void main() { builder: (context) { return optionsFilterChipInput.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'OptionsFilterChipInput', buildChild: (_, [_]) => const SizedBox.shrink(), dispatchEvent: (event) {}, buildContext: context, @@ -124,5 +128,58 @@ void main() { // Check if the data model is updated. expect(dataModel.getValue(DataPath('/price')), '\$\$\$'); }); + + testWidgets('renders correctly and handles selection with literal value ' + '(implicit binding)', (WidgetTester tester) async { + final dataModel = DataModel(); + final Map data = { + 'chipLabel': 'Price', + 'options': ['\$', '\$\$', '\$\$\$'], + 'value': '\$', + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (context) { + return optionsFilterChipInput.widgetBuilder( + CatalogItemContext( + getCatalogItem: (type) => null, + data: data, + id: 'testId', + type: 'OptionsFilterChipInput', + buildChild: (_, [_]) => const SizedBox.shrink(), + dispatchEvent: (event) {}, + buildContext: context, + dataContext: DataContext(dataModel, '/'), + getComponent: (String componentId) => null, + surfaceId: 'surface1', + ), + ); + }, + ), + ), + ), + ); + + // Check initial state (should show literal value). + expect(find.byType(FilterChip), findsOneWidget); + expect(find.text('\$'), findsOneWidget); + + // Tap the chip to open the modal bottom sheet. + await tester.tap(find.byType(FilterChip)); + await tester.pumpAndSettle(); + + // Tap an option. + await tester.tap(find.text('\$\$\$').last); + await tester.pumpAndSettle(); + + // Check if the chip label is updated. + expect(find.text('\$\$\$'), findsOneWidget); + + // Check if the data model is updated at implicit path. + expect(dataModel.getValue(DataPath('testId.value')), '\$\$\$'); + }); }); } diff --git a/examples/travel_app/test/tabbed_sections_test.dart b/examples/travel_app/test/tabbed_sections_test.dart index ac7e37709..bf38ffbef 100644 --- a/examples/travel_app/test/tabbed_sections_test.dart +++ b/examples/travel_app/test/tabbed_sections_test.dart @@ -26,14 +26,8 @@ void main() { final CatalogItem catalogItem = tabbedSections; final Map>> data = { 'sections': [ - { - 'title': {'literalString': 'Tab 1'}, - 'child': 'child1', - }, - { - 'title': {'literalString': 'Tab 2'}, - 'child': 'child2', - }, + {'title': 'Tab 1', 'child': 'child1'}, + {'title': 'Tab 2', 'child': 'child2'}, ], }; @@ -48,8 +42,10 @@ void main() { builder: (context) { return catalogItem.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'TabbedSections', buildChild: mockBuildChild, dispatchEvent: (event) {}, buildContext: context, diff --git a/examples/travel_app/test/trailhead_test.dart b/examples/travel_app/test/trailhead_test.dart index 2b32dbee7..15f367b72 100644 --- a/examples/travel_app/test/trailhead_test.dart +++ b/examples/travel_app/test/trailhead_test.dart @@ -13,11 +13,10 @@ void main() { WidgetTester tester, ) async { final Map data = { - 'topics': [ - {'literalString': 'Topic A'}, - {'literalString': 'Topic B'}, - ], - 'action': {'name': 'selectTopic'}, + 'topics': ['Topic A', 'Topic B'], + 'action': { + 'event': {'name': 'selectTopic'}, + }, }; UiEvent? dispatchedEvent; @@ -28,8 +27,10 @@ void main() { builder: (context) { return trailhead.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'Trailhead', buildChild: (_, [_]) => const SizedBox.shrink(), dispatchEvent: (event) { dispatchedEvent = event; @@ -64,7 +65,9 @@ void main() { ) async { final Map data = { 'topics': >[], - 'action': {'name': 'selectTopic'}, + 'action': { + 'event': {'name': 'selectTopic'}, + }, }; await tester.pumpWidget( @@ -74,8 +77,10 @@ void main() { builder: (context) { return trailhead.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'Trailhead', buildChild: (_, [_]) => const SizedBox.shrink(), dispatchEvent: (event) {}, buildContext: context, diff --git a/examples/travel_app/test/travel_carousel_test.dart b/examples/travel_app/test/travel_carousel_test.dart index 82b52a9e8..68d1b1544 100644 --- a/examples/travel_app/test/travel_carousel_test.dart +++ b/examples/travel_app/test/travel_carousel_test.dart @@ -17,14 +17,18 @@ void main() { final Map>> data = { 'items': [ { - 'description': {'literalString': 'Item 1'}, + 'description': 'Item 1', 'imageChildId': 'imageId1', - 'action': {'name': 'selectItem'}, + 'action': { + 'event': {'name': 'selectItem'}, + }, }, { - 'description': {'literalString': 'Item 2'}, + 'description': 'Item 2', 'imageChildId': 'imageId2', - 'action': {'name': 'selectItem'}, + 'action': { + 'event': {'name': 'selectItem'}, + }, }, ], }; @@ -41,8 +45,10 @@ void main() { builder: (context) { return travelCarousel.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'TravelCarousel', buildChild: buildChild, dispatchEvent: (event) { dispatchedEvent = event; @@ -81,15 +87,19 @@ void main() { final Map>> data = { 'items': [ { - 'description': {'literalString': 'Item 1'}, + 'description': 'Item 1', 'imageChildId': 'imageId1', 'listingSelectionId': 'listing1', - 'action': {'name': 'selectItem'}, + 'action': { + 'event': {'name': 'selectItem'}, + }, }, { - 'description': {'literalString': 'Item 2'}, + 'description': 'Item 2', 'imageChildId': 'imageId2', - 'action': {'name': 'selectItem'}, + 'action': { + 'event': {'name': 'selectItem'}, + }, }, ], }; @@ -106,8 +116,10 @@ void main() { builder: (context) { return travelCarousel.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'TravelCarousel', buildChild: buildChild, dispatchEvent: (event) { dispatchedEvent = event; @@ -146,8 +158,10 @@ void main() { builder: (context) { return travelCarousel.widgetBuilder( CatalogItemContext( + getCatalogItem: (type) => null, data: data, id: 'testId', + type: 'TravelCarousel', buildChild: (data, [_]) => Text(data), dispatchEvent: (event) {}, buildContext: context, diff --git a/examples/travel_app/test/widgets/conversation_test.dart b/examples/travel_app/test/widgets/conversation_test.dart index 1b37a4dff..acbdeea13 100644 --- a/examples/travel_app/test/widgets/conversation_test.dart +++ b/examples/travel_app/test/widgets/conversation_test.dart @@ -4,41 +4,44 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; +import 'package:genui/genui.dart' hide Conversation; + import 'package:travel_app/src/widgets/conversation.dart'; void main() { group('Conversation', () { - late A2uiMessageProcessor manager; + late SurfaceController manager; setUp(() { - manager = A2uiMessageProcessor(catalogs: [CoreCatalogItems.asCatalog()]); + manager = SurfaceController(catalogs: [BasicCatalogItems.asCatalog()]); }); testWidgets('renders a list of messages', (WidgetTester tester) async { const surfaceId = 's1'; - final List messages = [ - UserMessage.text('Hello'), - AiUiMessage( - surfaceId: surfaceId, - definition: UiDefinition(surfaceId: surfaceId), + final messages = [ + ChatMessage.user('Hello'), + ChatMessage.model( + '', + parts: [ + UiPart.create( + definition: SurfaceDefinition(surfaceId: surfaceId), + surfaceId: surfaceId, + ), + ], ), ]; final components = [ const Component( - id: 'r1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Hi there!'}, - }, - }, + id: 'root', + type: 'Text', + properties: {'text': 'Hi there!'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering(surfaceId: surfaceId, root: 'r1'), + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), ); await tester.pumpWidget( @@ -54,9 +57,7 @@ void main() { expect(find.text('Hi there!'), findsOneWidget); }); testWidgets('renders UserPrompt correctly', (WidgetTester tester) async { - final messages = [ - UserMessage([const TextPart('Hello')]), - ]; + final messages = [ChatMessage.user('Hello')]; await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -71,26 +72,28 @@ void main() { testWidgets('renders UiResponse correctly', (WidgetTester tester) async { const surfaceId = 's1'; final messages = [ - AiUiMessage( - surfaceId: surfaceId, - definition: UiDefinition(surfaceId: surfaceId), + ChatMessage.model( + '', + parts: [ + UiPart.create( + definition: SurfaceDefinition(surfaceId: surfaceId), + surfaceId: surfaceId, + ), + ], ), ]; final components = [ const Component( id: 'root', - componentProperties: { - 'Text': { - 'text': {'literalString': 'UI Content'}, - }, - }, + type: 'Text', + properties: {'text': 'UI Content'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering(surfaceId: surfaceId, root: 'root'), + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), ); await tester.pumpWidget( MaterialApp( @@ -100,14 +103,12 @@ void main() { ), ); await tester.pumpAndSettle(); - expect(find.byType(GenUiSurface), findsOneWidget); + expect(find.byType(Surface), findsOneWidget); expect(find.text('UI Content'), findsOneWidget); }); testWidgets('uses custom userPromptBuilder', (WidgetTester tester) async { - final messages = [ - UserMessage(const [TextPart('Hello')]), - ]; + final messages = [ChatMessage.user('Hello')]; await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -123,5 +124,26 @@ void main() { expect(find.text('Custom User Prompt'), findsOneWidget); expect(find.text('Hello'), findsNothing); }); + + testWidgets('renders user interaction correctly', ( + WidgetTester tester, + ) async { + final messages = [ + ChatMessage.user('', parts: [UiInteractionPart.create('{}')]), + ]; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Conversation( + messages: messages, + manager: manager, + userUiInteractionBuilder: (context, message) => + const Text('User Interaction'), + ), + ), + ), + ); + expect(find.text('User Interaction'), findsOneWidget); + }); }); } diff --git a/examples/travel_app/windows/flutter/generated_plugin_registrant.cc b/examples/travel_app/windows/flutter/generated_plugin_registrant.cc index d141b74f5..ec8e8d457 100644 --- a/examples/travel_app/windows/flutter/generated_plugin_registrant.cc +++ b/examples/travel_app/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,12 @@ #include "generated_plugin_registrant.h" -#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseAuthPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/examples/travel_app/windows/flutter/generated_plugins.cmake b/examples/travel_app/windows/flutter/generated_plugins.cmake index 29944d5b1..02d26c31b 100644 --- a/examples/travel_app/windows/flutter/generated_plugins.cmake +++ b/examples/travel_app/windows/flutter/generated_plugins.cmake @@ -3,8 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - firebase_auth firebase_core + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/verdure/client/lib/features/ai/ai_provider.dart b/examples/verdure/client/lib/features/ai/ai_provider.dart index 6592bfd81..7aac597ec 100644 --- a/examples/verdure/client/lib/features/ai/ai_provider.dart +++ b/examples/verdure/client/lib/features/ai/ai_provider.dart @@ -44,19 +44,19 @@ class AiClientState { /// Creates an [AiClientState]. AiClientState({ required this.a2uiMessageProcessor, - required this.contentGenerator, + required this.connector, required this.conversation, required this.surfaceUpdateController, }); /// The A2UI message processor. - final A2uiMessageProcessor a2uiMessageProcessor; + final SurfaceController a2uiMessageProcessor; - /// The content generator. - final A2uiContentGenerator contentGenerator; + /// The agent connector. + final A2uiAgentConnector connector; /// The conversation manager. - final GenUiConversation conversation; + final Conversation conversation; /// A stream that emits the ID of the most recently updated surface. final StreamController surfaceUpdateController; @@ -67,57 +67,59 @@ class AiClientState { class Ai extends _$Ai { @override Future build() async { - final a2uiMessageProcessor = A2uiMessageProcessor( - catalogs: [CoreCatalogItems.asCatalog()], + final a2uiMessageProcessor = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], ); final A2uiAgentConnector connector = await ref.watch( a2uiAgentConnectorProvider.future, ); - final String serverUrl = await ref.watch(a2aServerUrlProvider.future); - final contentGenerator = A2uiContentGenerator( - serverUrl: Uri.parse(serverUrl), - connector: connector, + + final controller = A2uiTransportAdapter( + onSend: (message) async { + // Send request via connector + await connector.connectAndSend(message); + }, ); - final conversation = GenUiConversation( - contentGenerator: contentGenerator, - a2uiMessageProcessor: a2uiMessageProcessor, + + // Wire up connector to controller + connector.stream.listen(controller.addMessage); + connector.textStream.listen(controller.addChunk); + + final conversation = Conversation( + transport: controller, + controller: a2uiMessageProcessor, ); + final surfaceUpdateController = StreamController.broadcast(); - contentGenerator.a2uiMessageStream.listen((message) { - switch (message) { - case BeginRendering(): - surfaceUpdateController.add(message.surfaceId); - case SurfaceUpdate(): - case DataModelUpdate(): - case SurfaceDeletion(): - // We only navigate on BeginRendering. + connector.stream.listen((message) { + if (message is CreateSurface) { + surfaceUpdateController.add(message.surfaceId); } }); // Fetch the agent card to initialize the connection. - await contentGenerator.connector.getAgentCard(); + await connector.getAgentCard(); void updateProcessingState() { LoadingState.instance.isProcessing.value = - contentGenerator.isProcessing.value; + conversation.state.value.isWaiting; } - contentGenerator.isProcessing.addListener(updateProcessingState); + conversation.state.addListener(updateProcessingState); ref.onDispose(() { - contentGenerator.isProcessing.removeListener(updateProcessingState); + conversation.state.removeListener(updateProcessingState); // Reset the loading state when the provider is disposed. LoadingState.instance.isProcessing.value = false; conversation.dispose(); - // contentGenerator is disposed by conversation.dispose(), so we don't - // need to dispose it again. + controller.dispose(); surfaceUpdateController.close(); }); return AiClientState( a2uiMessageProcessor: a2uiMessageProcessor, - contentGenerator: contentGenerator, + connector: connector, conversation: conversation, surfaceUpdateController: surfaceUpdateController, ); diff --git a/examples/verdure/client/lib/features/screens/order_confirmation_screen.dart b/examples/verdure/client/lib/features/screens/order_confirmation_screen.dart index 29063eb8e..05cb8cb55 100644 --- a/examples/verdure/client/lib/features/screens/order_confirmation_screen.dart +++ b/examples/verdure/client/lib/features/screens/order_confirmation_screen.dart @@ -26,16 +26,18 @@ class OrderConfirmationScreen extends ConsumerWidget { .watch(aiProvider) .when( data: (aiState) { - return ValueListenableBuilder( - valueListenable: aiState.a2uiMessageProcessor - .getSurfaceNotifier('confirmation'), + return ValueListenableBuilder( + valueListenable: aiState.a2uiMessageProcessor.watchSurface( + 'confirmation', + ), builder: (context, definition, child) { if (definition == null) { return const Center(child: CircularProgressIndicator()); } - return GenUiSurface( - host: aiState.a2uiMessageProcessor, - surfaceId: 'confirmation', + return Surface( + surfaceContext: aiState.a2uiMessageProcessor.contextFor( + 'confirmation', + ), ); }, ); diff --git a/examples/verdure/client/lib/features/screens/presentation_screen.dart b/examples/verdure/client/lib/features/screens/presentation_screen.dart index fec63acda..d9a8c3fbc 100644 --- a/examples/verdure/client/lib/features/screens/presentation_screen.dart +++ b/examples/verdure/client/lib/features/screens/presentation_screen.dart @@ -50,18 +50,20 @@ class _PresentationScreenState extends ConsumerState { .watch(aiProvider) .when( data: (aiState) { - return ValueListenableBuilder( - valueListenable: aiState.a2uiMessageProcessor - .getSurfaceNotifier('options'), + return ValueListenableBuilder( + valueListenable: aiState.a2uiMessageProcessor.watchSurface( + 'options', + ), builder: (context, definition, child) { if (definition == null) { return const Center(child: CircularProgressIndicator()); } return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), - child: GenUiSurface( - host: aiState.a2uiMessageProcessor, - surfaceId: 'options', + child: Surface( + surfaceContext: aiState.a2uiMessageProcessor.contextFor( + 'options', + ), ), ); }, diff --git a/examples/verdure/client/lib/features/screens/questionnaire_screen.dart b/examples/verdure/client/lib/features/screens/questionnaire_screen.dart index caf463d52..d03e756c0 100644 --- a/examples/verdure/client/lib/features/screens/questionnaire_screen.dart +++ b/examples/verdure/client/lib/features/screens/questionnaire_screen.dart @@ -31,7 +31,7 @@ class _QuestionnaireScreenState extends ConsumerState { _initialRequestSent = true; }); aiState.conversation.sendRequest( - UserMessage.text('USER_SUBMITTED_DETAILS'), + ChatMessage.user('USER_SUBMITTED_DETAILS'), ); } }); @@ -53,18 +53,20 @@ class _QuestionnaireScreenState extends ConsumerState { .watch(aiProvider) .when( data: (aiState) { - return ValueListenableBuilder( - valueListenable: aiState.a2uiMessageProcessor - .getSurfaceNotifier('questionnaire'), + return ValueListenableBuilder( + valueListenable: aiState.a2uiMessageProcessor.watchSurface( + 'questionnaire', + ), builder: (context, definition, child) { if (definition == null) { return const Center(child: CircularProgressIndicator()); } return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), - child: GenUiSurface( - host: aiState.a2uiMessageProcessor, - surfaceId: 'questionnaire', + child: Surface( + surfaceContext: aiState.a2uiMessageProcessor.contextFor( + 'questionnaire', + ), ), ); }, diff --git a/examples/verdure/client/lib/features/screens/shopping_cart_screen.dart b/examples/verdure/client/lib/features/screens/shopping_cart_screen.dart index 9e7fc6273..b077873ef 100644 --- a/examples/verdure/client/lib/features/screens/shopping_cart_screen.dart +++ b/examples/verdure/client/lib/features/screens/shopping_cart_screen.dart @@ -33,16 +33,18 @@ class ShoppingCartScreen extends ConsumerWidget { .watch(aiProvider) .when( data: (aiState) { - return ValueListenableBuilder( - valueListenable: aiState.a2uiMessageProcessor - .getSurfaceNotifier('cart'), + return ValueListenableBuilder( + valueListenable: aiState.a2uiMessageProcessor.watchSurface( + 'cart', + ), builder: (context, definition, child) { if (definition == null) { return const Center(child: CircularProgressIndicator()); } - return GenUiSurface( - host: aiState.a2uiMessageProcessor, - surfaceId: 'cart', + return Surface( + surfaceContext: aiState.a2uiMessageProcessor.contextFor( + 'cart', + ), ); }, ); diff --git a/examples/verdure/client/lib/features/screens/upload_photo_screen.dart b/examples/verdure/client/lib/features/screens/upload_photo_screen.dart index aa82e3e91..b303a690a 100644 --- a/examples/verdure/client/lib/features/screens/upload_photo_screen.dart +++ b/examples/verdure/client/lib/features/screens/upload_photo_screen.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -111,16 +112,24 @@ class UploadPhotoScreen extends ConsumerWidget { ref.read(aiProvider).whenData((aiState) { aiState.conversation.sendRequest( - UserMessage([ - const DataPart({ - 'userAction': { - 'name': 'submit_details', - 'sourceComponentId': 'upload_button', - 'context': {}, - }, - }), - ImagePart.fromBytes(bytes, mimeType: mimeType), - ]), + ChatMessage.user( + '', + parts: [ + DataPart( + utf8.encode( + jsonEncode({ + 'action': { + 'name': 'submit_details', + 'sourceComponentId': 'upload_button', + 'context': {}, + }, + }), + ), + mimeType: 'application/json', + ), + DataPart(bytes, mimeType: mimeType), + ], + ), ); }); } diff --git a/examples/verdure/client/lib/features/state/loading_state.dart b/examples/verdure/client/lib/features/state/loading_state.dart index 97e53e976..7eb6dd122 100644 --- a/examples/verdure/client/lib/features/state/loading_state.dart +++ b/examples/verdure/client/lib/features/state/loading_state.dart @@ -31,9 +31,8 @@ class LoadingState { LoadingState._() { // When the processing state changes from true to false, reset messages. isProcessing.addListener(() { - if (!_isProcessingValue && isProcessing.value) { - // Went from false to true - } else if (_isProcessingValue && !isProcessing.value) { + // Went from false to true: do nothing. + if (_isProcessingValue && !isProcessing.value) { // Went from true to false, reset messages after a short delay // to allow the fade-out animation to complete. Future.delayed(const Duration(milliseconds: 500), clearMessages); diff --git a/examples/verdure/client/lib/features/widgets/app_navigator.dart b/examples/verdure/client/lib/features/widgets/app_navigator.dart index 6fac8e573..f374b7d3a 100644 --- a/examples/verdure/client/lib/features/widgets/app_navigator.dart +++ b/examples/verdure/client/lib/features/widgets/app_navigator.dart @@ -33,13 +33,15 @@ class _AppNavigatorState extends ConsumerState { final AsyncValue aiState = ref.read(aiProvider); if (aiState case AsyncData(:final value)) { _subscription = value.surfaceUpdateController.stream.listen( - _onSurfaceUpdate, + _handleSurfaceNavigation, ); } } - void _onSurfaceUpdate(String surfaceId) { + void _handleSurfaceNavigation(String surfaceId) { switch (surfaceId) { + case 'details': + widget.router.push('/upload_photo'); case 'questionnaire': widget.router.push('/questionnaire'); case 'options': @@ -65,7 +67,7 @@ class _AppNavigatorState extends ConsumerState { if (next case AsyncData(:final value?)) { _subscription?.cancel(); _subscription = value.surfaceUpdateController.stream.listen( - _onSurfaceUpdate, + _handleSurfaceNavigation, ); } }); diff --git a/examples/verdure/client/linux/flutter/generated_plugin_registrant.cc b/examples/verdure/client/linux/flutter/generated_plugin_registrant.cc index 64a0ecea4..7299b5cf2 100644 --- a/examples/verdure/client/linux/flutter/generated_plugin_registrant.cc +++ b/examples/verdure/client/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/examples/verdure/client/linux/flutter/generated_plugins.cmake b/examples/verdure/client/linux/flutter/generated_plugins.cmake index 2db3c22ae..786ff5c29 100644 --- a/examples/verdure/client/linux/flutter/generated_plugins.cmake +++ b/examples/verdure/client/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift index 542a28a58..8caf5d583 100644 --- a/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/verdure/client/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import device_info_plus import file_selector_macos +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/examples/verdure/server/a2ui_extension/src/a2ui_ext/__init__.py b/examples/verdure/server/a2ui_extension/src/a2ui_ext/__init__.py index a228fb686..467b0f49f 100644 --- a/examples/verdure/server/a2ui_extension/src/a2ui_ext/__init__.py +++ b/examples/verdure/server/a2ui_extension/src/a2ui_ext/__init__.py @@ -22,7 +22,7 @@ logger = logging.getLogger(__name__) # --- Define a2ui UI constants --- -_API_VERSION = "v0.8" +_API_VERSION = "v0.9" _CORE_PATH = f"a2ui.org/a2a-extension/a2ui/{_API_VERSION}" URI = f"https://{_CORE_PATH}" a2ui_MIME_TYPE = "application/json+a2ui" diff --git a/examples/verdure/server/uv.lock b/examples/verdure/server/uv.lock index d5b8fba37..014bda880 100644 --- a/examples/verdure/server/uv.lock +++ b/examples/verdure/server/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" resolution-markers = [ "python_full_version >= '3.14'", @@ -16,7 +16,7 @@ members = [ [[package]] name = "a2a-sdk" version = "0.3.11" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core" }, { name = "httpx" }, @@ -24,9 +24,9 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/2c/6eff205080a4fb3937745f0bab4ff58716cdcc524acd077a493612d34336/a2a_sdk-0.3.11.tar.gz", hash = "sha256:194a6184d3e5c1c5d8941eb64fb33c346df3ebbec754effed8403f253bedb085", size = 226923, upload-time = "2025-11-07T11:05:38.496Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/a2a-sdk/a2a_sdk-0.3.11.tar.gz", hash = "sha256:194a6184d3e5c1c5d8941eb64fb33c346df3ebbec754effed8403f253bedb085" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f9/3e633485a3f23f5b3e04a7f0d3e690ae918fd1252941e8107c7593d882f1/a2a_sdk-0.3.11-py3-none-any.whl", hash = "sha256:f57673d5f38b3e0eb7c5b57e7dc126404d02c54c90692395ab4fd06aaa80cc8f", size = 140381, upload-time = "2025-11-07T11:05:37.093Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/a2a-sdk/a2a_sdk-0.3.11-py3-none-any.whl", hash = "sha256:f57673d5f38b3e0eb7c5b57e7dc126404d02c54c90692395ab4fd06aaa80cc8f" }, ] [[package]] @@ -70,16 +70,16 @@ requires-dist = [ [[package]] name = "aiohappyeyeballs" version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohappyeyeballs/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohappyeyeballs/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8" }, ] [[package]] name = "aiohttp" version = "3.13.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, @@ -89,271 +89,271 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, - { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, - { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, - { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, - { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, - { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, - { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, - { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, - { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, - { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, - { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, - { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, - { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, - { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, - { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, - { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, - { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, - { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, - { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, - { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiohttp/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f" }, ] [[package]] name = "aiosignal" version = "1.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiosignal/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/aiosignal/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e" }, ] [[package]] name = "alembic" version = "1.17.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "mako" }, { name = "sqlalchemy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/b6/2a81d7724c0c124edc5ec7a167e85858b6fd31b9611c6fb8ecf617b7e2d3/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486", size = 1981285, upload-time = "2025-10-29T00:23:16.667Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/alembic/alembic-1.17.1.tar.gz", hash = "sha256:8a289f6778262df31571d29cca4c7fbacd2f0f582ea0816f4c399b6da7528486" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/7df1d81ec2e50fb661944a35183d87e62d3f6c6d9f8aff64a4f245226d55/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023", size = 247848, upload-time = "2025-10-29T00:23:18.79Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/alembic/alembic-1.17.1-py3-none-any.whl", hash = "sha256:cbc2386e60f89608bb63f30d2d6cc66c7aaed1fe105bd862828600e5ad167023" }, ] [[package]] name = "annotated-doc" version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-doc/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320" }, ] [[package]] name = "annotated-types" version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/annotated-types/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53" }, ] [[package]] name = "anyio" version = "4.11.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "idna" }, { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/anyio/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc" }, ] [[package]] name = "attrs" version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/attrs/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" }, ] [[package]] name = "authlib" version = "1.6.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/authlib/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a" }, ] [[package]] name = "cachetools" version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cachetools/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cachetools/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701" }, ] [[package]] name = "certifi" version = "2025.10.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/certifi/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de" }, ] [[package]] name = "cffi" version = "2.0.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cffi/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/charset-normalizer/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f" }, ] [[package]] name = "click" version = "8.3.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/click/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/click/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc" }, ] [[package]] name = "cloudpickle" version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cloudpickle/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/cloudpickle/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a" }, ] [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/colorama/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, ] [[package]] @@ -412,161 +412,161 @@ wheels = [ [[package]] name = "distro" version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/distro/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2" }, ] [[package]] name = "docstring-parser" version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/docstring-parser/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/docstring-parser/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708" }, ] [[package]] name = "fastapi" version = "0.121.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441", size = 342523, upload-time = "2025-11-08T21:48:14.068Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc", size = 109192, upload-time = "2025-11-08T21:48:12.458Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastapi/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc" }, ] [[package]] name = "fastuuid" version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, - { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, - { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, - { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, - { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fastuuid/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a" }, ] [[package]] name = "filelock" version = "3.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/filelock/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/filelock/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2" }, ] [[package]] name = "frozenlist" version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/frozenlist/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d" }, ] [[package]] name = "fsspec" version = "2025.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fsspec/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/fsspec/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d" }, ] [[package]] name = "google-adk" version = "1.18.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, { name = "authlib" }, @@ -605,15 +605,15 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/7d/b331e2b31e32ca81f73111e9a79d1c6222d91f7b647013c77604a7f41322/google_adk-1.18.0.tar.gz", hash = "sha256:883fc621ce138099a75b2677017a1cd510e4303bad1415eabf38f802078d57b9", size = 1950454, upload-time = "2025-11-05T18:43:25.578Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-1.18.0.tar.gz", hash = "sha256:883fc621ce138099a75b2677017a1cd510e4303bad1415eabf38f802078d57b9" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/30/1012b25cb0dfb630c2b8c040c181f0b6559efb00d1e6007c4eca24970fb2/google_adk-1.18.0-py3-none-any.whl", hash = "sha256:657fe281718ce87117149f006556f9fd84a0bdbe1073dd6b8c3d4bd3e6044b45", size = 2244321, upload-time = "2025-11-05T18:43:23.987Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-adk/google_adk-1.18.0-py3-none-any.whl", hash = "sha256:657fe281718ce87117149f006556f9fd84a0bdbe1073dd6b8c3d4bd3e6044b45" }, ] [[package]] name = "google-api-core" version = "2.28.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, @@ -621,9 +621,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-core/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-core/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c" }, ] [package.optional-dependencies] @@ -635,7 +635,7 @@ grpc = [ [[package]] name = "google-api-python-client" version = "2.187.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, @@ -643,42 +643,42 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/83/60cdacf139d768dd7f0fcbe8d95b418299810068093fdf8228c6af89bb70/google_api_python_client-2.187.0.tar.gz", hash = "sha256:e98e8e8f49e1b5048c2f8276473d6485febc76c9c47892a8b4d1afa2c9ec8278", size = 14068154, upload-time = "2025-11-06T01:48:53.274Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-python-client/google_api_python_client-2.187.0.tar.gz", hash = "sha256:e98e8e8f49e1b5048c2f8276473d6485febc76c9c47892a8b4d1afa2c9ec8278" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/58/c1e716be1b055b504d80db2c8413f6c6a890a6ae218a65f178b63bc30356/google_api_python_client-2.187.0-py3-none-any.whl", hash = "sha256:d8d0f6d85d7d1d10bdab32e642312ed572bdc98919f72f831b44b9a9cebba32f", size = 14641434, upload-time = "2025-11-06T01:48:50.763Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-api-python-client/google_api_python_client-2.187.0-py3-none-any.whl", hash = "sha256:d8d0f6d85d7d1d10bdab32e642312ed572bdc98919f72f831b44b9a9cebba32f" }, ] [[package]] name = "google-auth" version = "2.43.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16" }, ] [[package]] name = "google-auth-httplib2" version = "0.2.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/83/7ef576d1c7ccea214e7b001e69c006bc75e058a3a1f2ab810167204b698b/google_auth_httplib2-0.2.1.tar.gz", hash = "sha256:5ef03be3927423c87fb69607b42df23a444e434ddb2555b73b3679793187b7de", size = 11086, upload-time = "2025-10-30T21:13:16.569Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth-httplib2/google_auth_httplib2-0.2.1.tar.gz", hash = "sha256:5ef03be3927423c87fb69607b42df23a444e434ddb2555b73b3679793187b7de" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/a7/ca23dd006255f70e2bc469d3f9f0c82ea455335bfd682ad4d677adc435de/google_auth_httplib2-0.2.1-py3-none-any.whl", hash = "sha256:1be94c611db91c01f9703e7f62b0a59bbd5587a95571c7b6fade510d648bc08b", size = 9525, upload-time = "2025-10-30T21:13:15.758Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-auth-httplib2/google_auth_httplib2-0.2.1-py3-none-any.whl", hash = "sha256:1be94c611db91c01f9703e7f62b0a59bbd5587a95571c7b6fade510d648bc08b" }, ] [[package]] name = "google-cloud-aiplatform" version = "1.126.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "docstring-parser" }, { name = "google-api-core", extra = ["grpc"] }, @@ -694,9 +694,9 @@ dependencies = [ { name = "shapely" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/36/f8e41679e6cb7ea6b50c0bfeaea0b9daf1475cafa152ad30456f6ec5471f/google_cloud_aiplatform-1.126.1.tar.gz", hash = "sha256:956706c587b817e36d5a16af5ab7f48c73dde76c71d660ecd4284f0339dc37d4", size = 9777644, upload-time = "2025-11-06T22:00:52.894Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-aiplatform/google_cloud_aiplatform-1.126.1.tar.gz", hash = "sha256:956706c587b817e36d5a16af5ab7f48c73dde76c71d660ecd4284f0339dc37d4" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/c6/3dc21f6182703d170624ed9f87894d35e1d51d1facbb471aa62cc255f233/google_cloud_aiplatform-1.126.1-py2.py3-none-any.whl", hash = "sha256:66d4daea95356d772ff026f13448ea80aa763dfd8daedc21d9ca36d0a1ee8a65", size = 8123682, upload-time = "2025-11-06T22:00:49.874Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-aiplatform/google_cloud_aiplatform-1.126.1-py2.py3-none-any.whl", hash = "sha256:66d4daea95356d772ff026f13448ea80aa763dfd8daedc21d9ca36d0a1ee8a65" }, ] [package.optional-dependencies] @@ -716,7 +716,7 @@ agent-engines = [ [[package]] name = "google-cloud-appengine-logging" version = "1.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -724,28 +724,28 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/260266e5fa7283b721bbef012f3223d514e2569446f56786fe0c80aa0fd4/google_cloud_appengine_logging-1.7.0.tar.gz", hash = "sha256:ea9ce73430cfc99f8957fd7df97733f9a759d4caab65e19d63a7474f012ffd94", size = 16729, upload-time = "2025-10-17T02:33:40.842Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-appengine-logging/google_cloud_appengine_logging-1.7.0.tar.gz", hash = "sha256:ea9ce73430cfc99f8957fd7df97733f9a759d4caab65e19d63a7474f012ffd94" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/45/99bb629a23639d868c693748598796d7f8e60f62289795b6f310d3328b19/google_cloud_appengine_logging-1.7.0-py3-none-any.whl", hash = "sha256:cfd28bc61a030008381a646d112ebe2734bf72abc8c12afc47d035a2c9b041fe", size = 16924, upload-time = "2025-10-17T02:30:48.802Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-appengine-logging/google_cloud_appengine_logging-1.7.0-py3-none-any.whl", hash = "sha256:cfd28bc61a030008381a646d112ebe2734bf72abc8c12afc47d035a2c9b041fe" }, ] [[package]] name = "google-cloud-audit-log" version = "0.4.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "googleapis-common-protos" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/d2/ad96950410f8a05e921a6da2e1a6ba4aeca674bbb5dda8200c3c7296d7ad/google_cloud_audit_log-0.4.0.tar.gz", hash = "sha256:8467d4dcca9f3e6160520c24d71592e49e874838f174762272ec10e7950b6feb", size = 44682, upload-time = "2025-10-17T02:33:44.641Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-audit-log/google_cloud_audit_log-0.4.0.tar.gz", hash = "sha256:8467d4dcca9f3e6160520c24d71592e49e874838f174762272ec10e7950b6feb" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/25/532886995f11102ad6de290496de5db227bd3a73827702445928ad32edcb/google_cloud_audit_log-0.4.0-py3-none-any.whl", hash = "sha256:6b88e2349df45f8f4cc0993b687109b1388da1571c502dc1417efa4b66ec55e0", size = 44890, upload-time = "2025-10-17T02:30:55.11Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-audit-log/google_cloud_audit_log-0.4.0-py3-none-any.whl", hash = "sha256:6b88e2349df45f8f4cc0993b687109b1388da1571c502dc1417efa4b66ec55e0" }, ] [[package]] name = "google-cloud-bigquery" version = "3.38.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -755,15 +755,15 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/b2/a17e40afcf9487e3d17db5e36728ffe75c8d5671c46f419d7b6528a5728a/google_cloud_bigquery-3.38.0.tar.gz", hash = "sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520", size = 503666, upload-time = "2025-09-17T20:33:33.47Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-bigquery/google_cloud_bigquery-3.38.0.tar.gz", hash = "sha256:8afcb7116f5eac849097a344eb8bfda78b7cfaae128e60e019193dd483873520" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/3c/c8cada9ec282b29232ed9aed5a0b5cca6cf5367cb2ffa8ad0d2583d743f1/google_cloud_bigquery-3.38.0-py3-none-any.whl", hash = "sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6", size = 259257, upload-time = "2025-09-17T20:33:31.404Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-bigquery/google_cloud_bigquery-3.38.0-py3-none-any.whl", hash = "sha256:e06e93ff7b245b239945ef59cb59616057598d369edac457ebf292bd61984da6" }, ] [[package]] name = "google-cloud-bigtable" version = "2.34.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -773,43 +773,43 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/20/8a29e1d5858ba76f443dc527a223e769347b915cb060a9f19250241aa38a/google_cloud_bigtable-2.34.0.tar.gz", hash = "sha256:773258b00cd3f9a3a35639cc38bd711f4f1418aaa0c8d70cb028978ed98dc2c2", size = 766606, upload-time = "2025-10-22T19:04:53.645Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-bigtable/google_cloud_bigtable-2.34.0.tar.gz", hash = "sha256:773258b00cd3f9a3a35639cc38bd711f4f1418aaa0c8d70cb028978ed98dc2c2" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/6d/aa44110504b4b9d125f1cc9715b72a178ebbe5cb79698e7a95893c391e56/google_cloud_bigtable-2.34.0-py3-none-any.whl", hash = "sha256:a4a8db4903840cd3f89fb19c060eea2e7c09c1265cb0538cfc11288dbc6000e4", size = 537041, upload-time = "2025-10-22T19:04:52.014Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-bigtable/google_cloud_bigtable-2.34.0-py3-none-any.whl", hash = "sha256:a4a8db4903840cd3f89fb19c060eea2e7c09c1265cb0538cfc11288dbc6000e4" }, ] [[package]] name = "google-cloud-core" version = "2.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-core/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-core/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc" }, ] [[package]] name = "google-cloud-discoveryengine" version = "0.13.12" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8f/cd/b33bbc4b096d937abee5ebfad3908b2bdc65acd1582191aa33beaa2b70a5/google_cloud_discoveryengine-0.13.12.tar.gz", hash = "sha256:d6b9f8fadd8ad0d2f4438231c5eb7772a317e9f59cafbcbadc19b5d54c609419", size = 3582382, upload-time = "2025-09-22T16:51:14.052Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-discoveryengine/google_cloud_discoveryengine-0.13.12.tar.gz", hash = "sha256:d6b9f8fadd8ad0d2f4438231c5eb7772a317e9f59cafbcbadc19b5d54c609419" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/70/607f6011648f603d35e60a16c34aee68a0b39510e4268d4859f3268684f9/google_cloud_discoveryengine-0.13.12-py3-none-any.whl", hash = "sha256:295f8c6df3fb26b90fb82c2cd6fbcf4b477661addcb19a94eea16463a5c4e041", size = 3337248, upload-time = "2025-09-22T16:50:57.375Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-discoveryengine/google_cloud_discoveryengine-0.13.12-py3-none-any.whl", hash = "sha256:295f8c6df3fb26b90fb82c2cd6fbcf4b477661addcb19a94eea16463a5c4e041" }, ] [[package]] name = "google-cloud-logging" version = "3.12.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -821,15 +821,15 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/9c/d42ecc94f795a6545930e5f846a7ae59ff685ded8bc086648dd2bee31a1a/google_cloud_logging-3.12.1.tar.gz", hash = "sha256:36efc823985055b203904e83e1c8f9f999b3c64270bcda39d57386ca4effd678", size = 289569, upload-time = "2025-04-22T20:50:24.71Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-logging/google_cloud_logging-3.12.1.tar.gz", hash = "sha256:36efc823985055b203904e83e1c8f9f999b3c64270bcda39d57386ca4effd678" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/41/f8a3197d39b773a91f335dee36c92ef26a8ec96efe78d64baad89d367df4/google_cloud_logging-3.12.1-py2.py3-none-any.whl", hash = "sha256:6817878af76ec4e7568976772839ab2c43ddfd18fbbf2ce32b13ef549cd5a862", size = 229466, upload-time = "2025-04-22T20:50:23.294Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-logging/google_cloud_logging-3.12.1-py2.py3-none-any.whl", hash = "sha256:6817878af76ec4e7568976772839ab2c43ddfd18fbbf2ce32b13ef549cd5a862" }, ] [[package]] name = "google-cloud-monitoring" version = "2.28.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -837,15 +837,15 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/b8/7f68a7738cbfef610af532b2fc758e39d852fc93ed3a31bd0e76fd45d2fd/google_cloud_monitoring-2.28.0.tar.gz", hash = "sha256:25175590907e038add644b5b744941d221776342924637095a879973a7c0ac37", size = 393321, upload-time = "2025-10-14T15:42:55.786Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-monitoring/google_cloud_monitoring-2.28.0.tar.gz", hash = "sha256:25175590907e038add644b5b744941d221776342924637095a879973a7c0ac37" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/d3/02dcf5376cb4b47b9c06eba36d80700d5b0a1510f3fcd47d3abbe4b0f0a3/google_cloud_monitoring-2.28.0-py3-none-any.whl", hash = "sha256:64f4c57cc465dd51cceffe559f0ec6fa9f96aa6d82790cd8d3af6d5cc3795160", size = 384670, upload-time = "2025-10-14T15:42:41.911Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-monitoring/google_cloud_monitoring-2.28.0-py3-none-any.whl", hash = "sha256:64f4c57cc465dd51cceffe559f0ec6fa9f96aa6d82790cd8d3af6d5cc3795160" }, ] [[package]] name = "google-cloud-resource-manager" version = "1.15.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -854,15 +854,15 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/19/b95d0e8814ce42522e434cdd85c0cb6236d874d9adf6685fc8e6d1fda9d1/google_cloud_resource_manager-1.15.0.tar.gz", hash = "sha256:3d0b78c3daa713f956d24e525b35e9e9a76d597c438837171304d431084cedaf", size = 449227, upload-time = "2025-10-20T14:57:01.108Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-resource-manager/google_cloud_resource_manager-1.15.0.tar.gz", hash = "sha256:3d0b78c3daa713f956d24e525b35e9e9a76d597c438837171304d431084cedaf" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/93/5aef41a5f146ad4559dd7040ae5fa8e7ddcab4dfadbef6cb4b66d775e690/google_cloud_resource_manager-1.15.0-py3-none-any.whl", hash = "sha256:0ccde5db644b269ddfdf7b407a2c7b60bdbf459f8e666344a5285601d00c7f6d", size = 397151, upload-time = "2025-10-20T14:53:45.409Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-resource-manager/google_cloud_resource_manager-1.15.0-py3-none-any.whl", hash = "sha256:0ccde5db644b269ddfdf7b407a2c7b60bdbf459f8e666344a5285601d00c7f6d" }, ] [[package]] name = "google-cloud-secret-manager" version = "2.25.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -871,15 +871,15 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/7c/be2d11415eec83c400d315cf9876ba29742bc7af90df391d357763463cd2/google_cloud_secret_manager-2.25.0.tar.gz", hash = "sha256:a3792bb1cb307326908297a61536031ac94852c22248f04ae112ff51a853b561", size = 269853, upload-time = "2025-10-14T15:42:59.511Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-secret-manager/google_cloud_secret_manager-2.25.0.tar.gz", hash = "sha256:a3792bb1cb307326908297a61536031ac94852c22248f04ae112ff51a853b561" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/74/bf87966a6ee48c98d1b8a6a1839256911e9a2a205be76b21e54f58171615/google_cloud_secret_manager-2.25.0-py3-none-any.whl", hash = "sha256:eaf1adce3ff5dc0f24335709eba3410dc7e9d20aeea3e8df5b758e27080ebf14", size = 218548, upload-time = "2025-10-14T15:42:47.839Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-secret-manager/google_cloud_secret_manager-2.25.0-py3-none-any.whl", hash = "sha256:eaf1adce3ff5dc0f24335709eba3410dc7e9d20aeea3e8df5b758e27080ebf14" }, ] [[package]] name = "google-cloud-spanner" version = "3.59.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-cloud-core" }, @@ -889,15 +889,15 @@ dependencies = [ { name = "protobuf" }, { name = "sqlparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/62/f0e535875e49b34128710342115681fe1a97f45759e1427307ab150a4caa/google_cloud_spanner-3.59.0.tar.gz", hash = "sha256:dec7a78bfe1f94aef508ff9c61dba4196f3c70c83a0f75c271b4652686d08641", size = 705137, upload-time = "2025-10-23T09:35:49.885Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-spanner/google_cloud_spanner-3.59.0.tar.gz", hash = "sha256:dec7a78bfe1f94aef508ff9c61dba4196f3c70c83a0f75c271b4652686d08641" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/08/1a38139853364b4737e3a0e03a3fd87d60c7545e90a963a8a6457777b5f9/google_cloud_spanner-3.59.0-py3-none-any.whl", hash = "sha256:409ed9746787c9435fd015731a5e3cf6f3ea2995a807c580f4216bb5d464260a", size = 502645, upload-time = "2025-10-23T09:35:47.954Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-spanner/google_cloud_spanner-3.59.0-py3-none-any.whl", hash = "sha256:409ed9746787c9435fd015731a5e3cf6f3ea2995a807c580f4216bb5d464260a" }, ] [[package]] name = "google-cloud-speech" version = "2.34.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -905,15 +905,15 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/c2/500c58a7e3008cb77da01a2f2a8284ac55c808545d18551c62a031ff548d/google_cloud_speech-2.34.0.tar.gz", hash = "sha256:2a7bffd84f134b9b70c9f11cbb5088c534f92be149d71d9073d0b9dd3a431acf", size = 391496, upload-time = "2025-10-20T14:57:17.127Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-speech/google_cloud_speech-2.34.0.tar.gz", hash = "sha256:2a7bffd84f134b9b70c9f11cbb5088c534f92be149d71d9073d0b9dd3a431acf" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/4c/8c52951a4078f4b181917c37a2610e69c0b24a10567d0182bf089a933c35/google_cloud_speech-2.34.0-py3-none-any.whl", hash = "sha256:cc0c6c0fda9306fee01c998bc207b68f71e0a3247121a5a3a27daabacd3a8c98", size = 336614, upload-time = "2025-10-20T14:54:05.004Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-speech/google_cloud_speech-2.34.0-py3-none-any.whl", hash = "sha256:cc0c6c0fda9306fee01c998bc207b68f71e0a3247121a5a3a27daabacd3a8c98" }, ] [[package]] name = "google-cloud-storage" version = "3.5.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, @@ -922,15 +922,15 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/98/c0c6d10f893509585c755a6567689e914df3501ae269f46b0d67d7e7c70a/google_cloud_storage-3.5.0.tar.gz", hash = "sha256:10b89e1d1693114b3e0ca921bdd28c5418701fd092e39081bb77e5cee0851ab7", size = 17242207, upload-time = "2025-11-05T12:41:02.715Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-storage/google_cloud_storage-3.5.0.tar.gz", hash = "sha256:10b89e1d1693114b3e0ca921bdd28c5418701fd092e39081bb77e5cee0851ab7" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/81/a567236070e7fe79a17a11b118d7f5ce4adefe2edd18caf1824d7e29a30a/google_cloud_storage-3.5.0-py3-none-any.whl", hash = "sha256:e28fd6ad8764e60dbb9a398a7bc3296e7920c494bc329057d828127e5f9630d3", size = 289998, upload-time = "2025-11-05T12:41:01.212Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-storage/google_cloud_storage-3.5.0-py3-none-any.whl", hash = "sha256:e28fd6ad8764e60dbb9a398a7bc3296e7920c494bc329057d828127e5f9630d3" }, ] [[package]] name = "google-cloud-trace" version = "1.17.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, { name = "google-auth" }, @@ -938,30 +938,30 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/89/5ecbcf7d2d37ead01fc84e774bc758638855c630b32720fa58edcf9667ae/google_cloud_trace-1.17.0.tar.gz", hash = "sha256:68703bfc93718083f061d9130a3852e3181ec1b6b796b76856997c28f51b9595", size = 97995, upload-time = "2025-10-20T14:57:28.662Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-trace/google_cloud_trace-1.17.0.tar.gz", hash = "sha256:68703bfc93718083f061d9130a3852e3181ec1b6b796b76856997c28f51b9595" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/84/e6b776f0b5d68451be68d3d43efe8eacc677182709dd7e84c960668a9909/google_cloud_trace-1.17.0-py3-none-any.whl", hash = "sha256:975dc0c2a9b1d7644bca45d78a2c5011ab5c73e94bd6537203deda374f88f7b3", size = 104118, upload-time = "2025-10-20T14:55:23.108Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-cloud-trace/google_cloud_trace-1.17.0-py3-none-any.whl", hash = "sha256:975dc0c2a9b1d7644bca45d78a2c5011ab5c73e94bd6537203deda374f88f7b3" }, ] [[package]] name = "google-crc32c" version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495, upload-time = "2025-03-26T14:29:13.32Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/72/b8d785e9184ba6297a8620c8a37cf6e39b81a8ca01bb0796d7cbb28b3386/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35", size = 30467, upload-time = "2025-03-26T14:36:06.909Z" }, - { url = "https://files.pythonhosted.org/packages/34/25/5f18076968212067c4e8ea95bf3b69669f9fc698476e5f5eb97d5b37999f/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638", size = 30309, upload-time = "2025-03-26T15:06:15.318Z" }, - { url = "https://files.pythonhosted.org/packages/92/83/9228fe65bf70e93e419f38bdf6c5ca5083fc6d32886ee79b450ceefd1dbd/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb", size = 33133, upload-time = "2025-03-26T14:41:34.388Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ca/1ea2fd13ff9f8955b85e7956872fdb7050c4ace8a2306a6d177edb9cf7fe/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6", size = 32773, upload-time = "2025-03-26T14:41:35.19Z" }, - { url = "https://files.pythonhosted.org/packages/89/32/a22a281806e3ef21b72db16f948cad22ec68e4bdd384139291e00ff82fe2/google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db", size = 33475, upload-time = "2025-03-26T14:29:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c5/002975aff514e57fc084ba155697a049b3f9b52225ec3bc0f542871dd524/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3", size = 33243, upload-time = "2025-03-26T14:41:35.975Z" }, - { url = "https://files.pythonhosted.org/packages/61/cb/c585282a03a0cea70fcaa1bf55d5d702d0f2351094d663ec3be1c6c67c52/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9", size = 32870, upload-time = "2025-03-26T14:41:37.08Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-crc32c/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9" }, ] [[package]] name = "google-genai" version = "1.49.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, { name = "google-auth" }, @@ -972,33 +972,33 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/49/1a724ee3c3748fa50721d53a52d9fee88c67d0c43bb16eb2b10ee89ab239/google_genai-1.49.0.tar.gz", hash = "sha256:35eb16023b72e298571ae30e919c810694f258f2ba68fc77a2185c7c8829ad5a", size = 253493, upload-time = "2025-11-05T22:41:03.278Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-1.49.0.tar.gz", hash = "sha256:35eb16023b72e298571ae30e919c810694f258f2ba68fc77a2185c7c8829ad5a" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/d3/84a152746dc7bdebb8ba0fd7d6157263044acd1d14b2a53e8df4a307b6b7/google_genai-1.49.0-py3-none-any.whl", hash = "sha256:ad49cd5be5b63397069e7aef9a4fe0a84cbdf25fcd93408e795292308db4ef32", size = 256098, upload-time = "2025-11-05T22:41:01.429Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-genai/google_genai-1.49.0-py3-none-any.whl", hash = "sha256:ad49cd5be5b63397069e7aef9a4fe0a84cbdf25fcd93408e795292308db4ef32" }, ] [[package]] name = "google-resumable-media" version = "2.7.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099, upload-time = "2024-08-07T22:20:38.555Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-resumable-media/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251, upload-time = "2024-08-07T22:20:36.409Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/google-resumable-media/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa" }, ] [[package]] name = "googleapis-common-protos" version = "1.72.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/googleapis-common-protos/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038" }, ] [package.optional-dependencies] @@ -1009,202 +1009,202 @@ grpc = [ [[package]] name = "graphviz" version = "0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/graphviz/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42" }, ] [[package]] name = "greenlet" version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" }, - { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, - { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, - { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, - { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, - { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" }, - { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/greenlet/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01" }, ] [[package]] name = "grpc-google-iam-v1" version = "0.14.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpc-google-iam-v1/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpc-google-iam-v1/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6" }, ] [[package]] name = "grpc-interceptor" version = "0.15.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "grpcio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/28/57449d5567adf4c1d3e216aaca545913fbc21a915f2da6790d6734aac76e/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926", size = 19322, upload-time = "2023-11-16T02:05:42.459Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpc-interceptor/grpc-interceptor-0.15.4.tar.gz", hash = "sha256:1f45c0bcb58b6f332f37c637632247c9b02bc6af0fdceb7ba7ce8d2ebbfb0926" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/ac/8d53f230a7443401ce81791ec50a3b0e54924bf615ad287654fa4a2f5cdc/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d", size = 20848, upload-time = "2023-11-16T02:05:40.913Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpc-interceptor/grpc_interceptor-0.15.4-py3-none-any.whl", hash = "sha256:0035f33228693ed3767ee49d937bac424318db173fef4d2d0170b3215f254d9d" }, ] [[package]] name = "grpcio" version = "1.76.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, - { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, - { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, - { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, - { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, - { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, - { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, - { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, - { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e" }, ] [[package]] name = "grpcio-status" version = "1.76.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio-status/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/grpcio-status/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18" }, ] [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/h11/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" }, ] [[package]] name = "hf-xet" version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/hf-xet/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69" }, ] [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpcore/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55" }, ] [[package]] name = "httplib2" version = "0.31.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/77/6653db69c1f7ecfe5e3f9726fdadc981794656fcd7d98c4209fecfea9993/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c", size = 250759, upload-time = "2025-09-11T12:16:03.403Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httplib2/httplib2-0.31.0.tar.gz", hash = "sha256:ac7ab497c50975147d4f7b1ade44becc7df2f8954d42b38b3d69c515f531135c" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httplib2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24" }, ] [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpx/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://us-python.pkg.dev/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 = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpx-sse/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/httpx-sse/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc" }, ] [[package]] name = "huggingface-hub" version = "1.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, @@ -1217,126 +1217,126 @@ dependencies = [ { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/63/eeea214a6b456d8e91ac2ea73ebb83da3af9aa64716dfb6e28dd9b2e6223/huggingface_hub-1.1.2.tar.gz", hash = "sha256:7bdafc432dc12fa1f15211bdfa689a02531d2a47a3cc0d74935f5726cdbcab8e", size = 606173, upload-time = "2025-11-06T10:04:38.398Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/huggingface-hub/huggingface_hub-1.1.2.tar.gz", hash = "sha256:7bdafc432dc12fa1f15211bdfa689a02531d2a47a3cc0d74935f5726cdbcab8e" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/21/e15d90fd09b56938502a0348d566f1915f9789c5bb6c00c1402dc7259b6e/huggingface_hub-1.1.2-py3-none-any.whl", hash = "sha256:dfcfa84a043466fac60573c3e4af475490a7b0d7375b22e3817706d6659f61f7", size = 514955, upload-time = "2025-11-06T10:04:36.674Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/huggingface-hub/huggingface_hub-1.1.2-py3-none-any.whl", hash = "sha256:dfcfa84a043466fac60573c3e4af475490a7b0d7375b22e3817706d6659f61f7" }, ] [[package]] name = "idna" version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/idna/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea" }, ] [[package]] name = "importlib-metadata" version = "8.7.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/importlib-metadata/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/importlib-metadata/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" }, ] [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jinja2/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jinja2/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" }, ] [[package]] name = "jiter" version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jiter/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473" }, ] [[package]] name = "jsonschema" version = "4.25.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63" }, ] [[package]] name = "jsonschema-specifications" version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/jsonschema-specifications/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe" }, ] [[package]] name = "litellm" version = "1.79.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "aiohttp" }, { name = "click" }, @@ -1351,79 +1351,79 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/0a/587c3f895f5d6c842d6cd630204c8bf7de677fc69ce2bd26e812c02b6e0b/litellm-1.79.3.tar.gz", hash = "sha256:4da4716f8da3e1b77838262c36d3016146860933e0489171658a9d4a3fd59b1b", size = 11319885, upload-time = "2025-11-09T02:33:17.684Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/litellm/litellm-1.79.3.tar.gz", hash = "sha256:4da4716f8da3e1b77838262c36d3016146860933e0489171658a9d4a3fd59b1b" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/ad/3e030c925c99b9a2f1573bf376259338b502ed1aa25ae768bf1f79d8b1bf/litellm-1.79.3-py3-none-any.whl", hash = "sha256:16314049d109e5cadb2abdccaf2e07ea03d2caa3a9b3f54f34b5b825092b4eeb", size = 10412553, upload-time = "2025-11-09T02:33:14.021Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/litellm/litellm-1.79.3-py3-none-any.whl", hash = "sha256:16314049d109e5cadb2abdccaf2e07ea03d2caa3a9b3f54f34b5b825092b4eeb" }, ] [[package]] name = "mako" version = "1.3.10" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/mako/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/mako/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" }, ] [[package]] name = "markupsafe" version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/markupsafe/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa" }, ] [[package]] name = "mcp" -version = "1.23.0" -source = { registry = "https://pypi.org/simple" } +version = "1.21.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, { name = "httpx" }, @@ -1436,152 +1436,150 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/1a/9c8a5362e3448d585081d6c7aa95898a64e0ac59d3e26169ae6c3ca5feaf/mcp-1.23.0.tar.gz", hash = "sha256:84e0c29316d0a8cf0affd196fd000487ac512aa3f771b63b2ea864e22961772b", size = 596506, upload-time = "2025-12-02T13:40:02.558Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/mcp/mcp-1.21.0.tar.gz", hash = "sha256:bab0a38e8f8c48080d787233343f8d301b0e1e95846ae7dead251b2421d99855" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/b2/28739ce409f98159c0121eab56e69ad71546c4f34ac8b42e58c03f57dccc/mcp-1.23.0-py3-none-any.whl", hash = "sha256:5a645cf111ed329f4619f2629a3f15d9aabd7adc2ea09d600d31467b51ecb64f", size = 231427, upload-time = "2025-12-02T13:40:00.738Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/mcp/mcp-1.21.0-py3-none-any.whl", hash = "sha256:598619e53eb0b7a6513db38c426b28a4bdf57496fed04332100d2c56acade98b" }, ] [[package]] name = "multidict" version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/multidict/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3" }, ] [[package]] name = "numpy" version = "2.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, - { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, - { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, - { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, - { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, - { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, - { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, - { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, - { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, - { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, - { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, - { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, - { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, - { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, - { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, - { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, - { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, - { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, - { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, - { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/numpy/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29" }, ] [[package]] name = "openai" version = "2.7.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, { name = "distro" }, @@ -1592,85 +1590,85 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/e3/cec27fa28ef36c4ccea71e9e8c20be9b8539618732989a82027575aab9d4/openai-2.7.2.tar.gz", hash = "sha256:082ef61163074d8efad0035dd08934cf5e3afd37254f70fc9165dd6a8c67dcbd", size = 595732, upload-time = "2025-11-10T16:42:31.108Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/openai/openai-2.7.2.tar.gz", hash = "sha256:082ef61163074d8efad0035dd08934cf5e3afd37254f70fc9165dd6a8c67dcbd" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/66/22cfe4b695b5fd042931b32c67d685e867bfd169ebf46036b95b57314c33/openai-2.7.2-py3-none-any.whl", hash = "sha256:116f522f4427f8a0a59b51655a356da85ce092f3ed6abeca65f03c8be6e073d9", size = 1008375, upload-time = "2025-11-10T16:42:28.574Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/openai/openai-2.7.2-py3-none-any.whl", hash = "sha256:116f522f4427f8a0a59b51655a356da85ce092f3ed6abeca65f03c8be6e073d9" }, ] [[package]] name = "opentelemetry-api" version = "1.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923, upload-time = "2025-09-11T10:29:01.662Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732, upload-time = "2025-09-11T10:28:41.826Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-api/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47" }, ] [[package]] name = "opentelemetry-exporter-gcp-logging" version = "1.11.0a0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-cloud-logging" }, { name = "opentelemetry-api" }, { name = "opentelemetry-resourcedetector-gcp" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/2d/6aa7063b009768d8f9415b36a29ae9b3eb1e2c5eff70f58ca15e104c245f/opentelemetry_exporter_gcp_logging-1.11.0a0.tar.gz", hash = "sha256:58496f11b930c84570060ffbd4343cd0b597ea13c7bc5c879df01163dd552f14", size = 22400, upload-time = "2025-11-04T19:32:13.812Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-logging/opentelemetry_exporter_gcp_logging-1.11.0a0.tar.gz", hash = "sha256:58496f11b930c84570060ffbd4343cd0b597ea13c7bc5c879df01163dd552f14" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/b7/2d3df53fa39bfd52f88c78a60367d45a7b1adbf8a756cce62d6ac149d49a/opentelemetry_exporter_gcp_logging-1.11.0a0-py3-none-any.whl", hash = "sha256:f8357c552947cb9c0101c4575a7702b8d3268e28bdeefdd1405cf838e128c6ef", size = 14168, upload-time = "2025-11-04T19:32:07.073Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-logging/opentelemetry_exporter_gcp_logging-1.11.0a0-py3-none-any.whl", hash = "sha256:f8357c552947cb9c0101c4575a7702b8d3268e28bdeefdd1405cf838e128c6ef" }, ] [[package]] name = "opentelemetry-exporter-gcp-monitoring" version = "1.11.0a0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-cloud-monitoring" }, { name = "opentelemetry-api" }, { name = "opentelemetry-resourcedetector-gcp" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/48/d1c7d2380bb1754d1eb6a011a2e0de08c6868cb6c0f34bcda0444fa0d614/opentelemetry_exporter_gcp_monitoring-1.11.0a0.tar.gz", hash = "sha256:386276eddbbd978a6f30fafd3397975beeb02a1302bdad554185242a8e2c343c", size = 20828, upload-time = "2025-11-04T19:32:14.522Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-monitoring/opentelemetry_exporter_gcp_monitoring-1.11.0a0.tar.gz", hash = "sha256:386276eddbbd978a6f30fafd3397975beeb02a1302bdad554185242a8e2c343c" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/8c/03a6e73e270a9c890dbd6cc1c47c83d86b8a8a974a9168d92e043c6277cc/opentelemetry_exporter_gcp_monitoring-1.11.0a0-py3-none-any.whl", hash = "sha256:b6740cba61b2f9555274829fe87a58447b64d0378f1067a4faebb4f5b364ca22", size = 13611, upload-time = "2025-11-04T19:32:08.212Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-monitoring/opentelemetry_exporter_gcp_monitoring-1.11.0a0-py3-none-any.whl", hash = "sha256:b6740cba61b2f9555274829fe87a58447b64d0378f1067a4faebb4f5b364ca22" }, ] [[package]] name = "opentelemetry-exporter-gcp-trace" version = "1.11.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "google-cloud-trace" }, { name = "opentelemetry-api" }, { name = "opentelemetry-resourcedetector-gcp" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/9c/4c3b26e5494f8b53c7873732a2317df905abe2b8ab33e9edfcbd5a8ff79b/opentelemetry_exporter_gcp_trace-1.11.0.tar.gz", hash = "sha256:c947ab4ab53e16517ade23d6fe71fe88cf7ca3f57a42c9f0e4162d2b929fecb6", size = 18770, upload-time = "2025-11-04T19:32:15.109Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-trace/opentelemetry_exporter_gcp_trace-1.11.0.tar.gz", hash = "sha256:c947ab4ab53e16517ade23d6fe71fe88cf7ca3f57a42c9f0e4162d2b929fecb6" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/4a/876703e8c5845198d95cd4006c8d1b2e3b129a9e288558e33133360f8d5d/opentelemetry_exporter_gcp_trace-1.11.0-py3-none-any.whl", hash = "sha256:b3dcb314e1a9985e9185cb7720b693eb393886fde98ae4c095ffc0893de6cefa", size = 14016, upload-time = "2025-11-04T19:32:09.009Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-gcp-trace/opentelemetry_exporter_gcp_trace-1.11.0-py3-none-any.whl", hash = "sha256:b3dcb314e1a9985e9185cb7720b693eb393886fde98ae4c095ffc0893de6cefa" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430, upload-time = "2025-09-11T10:29:03.605Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-otlp-proto-common/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359, upload-time = "2025-09-11T10:28:44.939Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-otlp-proto-common/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" version = "1.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, @@ -1680,289 +1678,289 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/e3/6e320aeb24f951449e73867e53c55542bebbaf24faeee7623ef677d66736/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac", size = 17281, upload-time = "2025-09-11T10:29:04.844Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-otlp-proto-http/opentelemetry_exporter_otlp_proto_http-1.37.0.tar.gz", hash = "sha256:e52e8600f1720d6de298419a802108a8f5afa63c96809ff83becb03f874e44ac" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/e9/70d74a664d83976556cec395d6bfedd9b85ec1498b778367d5f93e373397/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef", size = 19576, upload-time = "2025-09-11T10:28:46.726Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-exporter-otlp-proto-http/opentelemetry_exporter_otlp_proto_http-1.37.0-py3-none-any.whl", hash = "sha256:54c42b39945a6cc9d9a2a33decb876eabb9547e0dcb49df090122773447f1aef" }, ] [[package]] name = "opentelemetry-proto" version = "1.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151, upload-time = "2025-09-11T10:29:11.04Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-proto/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534, upload-time = "2025-09-11T10:28:56.831Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-proto/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2" }, ] [[package]] name = "opentelemetry-resourcedetector-gcp" version = "1.11.0a0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/5d/2b3240d914b87b6dd9cd5ca2ef1ccaf1d0626b897d4c06877e22c8c10fcf/opentelemetry_resourcedetector_gcp-1.11.0a0.tar.gz", hash = "sha256:915a1d6fd15daca9eedd3fc52b0f705375054f2ef140e2e7a6b4cca95a47cdb1", size = 18796, upload-time = "2025-11-04T19:32:16.59Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-resourcedetector-gcp/opentelemetry_resourcedetector_gcp-1.11.0a0.tar.gz", hash = "sha256:915a1d6fd15daca9eedd3fc52b0f705375054f2ef140e2e7a6b4cca95a47cdb1" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/6c/1e13fe142a7ca3dc6489167203a1209d32430cca12775e1df9c9a41c54b2/opentelemetry_resourcedetector_gcp-1.11.0a0-py3-none-any.whl", hash = "sha256:5d65a2a039b1d40c6f41421dbb08d5f441368275ac6de6e76a8fccd1f6acb67e", size = 18798, upload-time = "2025-11-04T19:32:10.915Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-resourcedetector-gcp/opentelemetry_resourcedetector_gcp-1.11.0a0-py3-none-any.whl", hash = "sha256:5d65a2a039b1d40c6f41421dbb08d5f441368275ac6de6e76a8fccd1f6acb67e" }, ] [[package]] name = "opentelemetry-sdk" version = "1.37.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404, upload-time = "2025-09-11T10:29:11.779Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941, upload-time = "2025-09-11T10:28:57.83Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-sdk/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c" }, ] [[package]] name = "opentelemetry-semantic-conventions" version = "0.58b0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867, upload-time = "2025-09-11T10:29:12.597Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/opentelemetry-semantic-conventions/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28" }, ] [[package]] name = "packaging" version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/packaging/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484" }, ] [[package]] name = "propcache" version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/propcache/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237" }, ] [[package]] name = "proto-plus" version = "1.26.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/proto-plus/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66" }, ] [[package]] name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +version = "6.33.0" +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0.tar.gz", hash = "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp310-abi3-win32.whl", hash = "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp310-abi3-win_amd64.whl", hash = "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/protobuf/protobuf-6.33.0-py3-none-any.whl", hash = "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995" }, ] [[package]] name = "pyasn1" version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629" }, ] [[package]] name = "pyasn1-modules" version = "0.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyasn1-modules/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, + { url = "https://us-python.pkg.dev/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 = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pycparser/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934" }, ] [[package]] name = "pydantic" version = "2.12.4" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e" }, ] [[package]] name = "pydantic-core" version = "2.41.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-core/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008" }, ] [[package]] name = "pydantic-settings" version = "2.12.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-settings/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pydantic-settings/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809" }, ] [[package]] name = "pyjwt" version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyjwt/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyjwt/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb" }, ] [package.optional-dependencies] @@ -1973,364 +1971,364 @@ crypto = [ [[package]] name = "pyparsing" version = "3.2.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyparsing/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyparsing/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dateutil/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dateutil/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" }, ] [[package]] name = "python-dotenv" version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-dotenv/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61" }, ] [[package]] name = "python-multipart" version = "0.0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/python-multipart/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104" }, ] [[package]] name = "pywin32" version = "311" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pywin32/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42" }, ] [[package]] name = "pyyaml" version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/pyyaml/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9" }, + { url = "https://us-python.pkg.dev/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 = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/referencing/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231" }, ] [[package]] name = "regex" version = "2025.11.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, - { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, - { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, - { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, - { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, - { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, - { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, - { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, - { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, - { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, - { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, - { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, - { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, - { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, - { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, - { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, - { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, - { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, - { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, - { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, - { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, - { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, - { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/regex/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801" }, ] [[package]] name = "requests" version = "2.32.5" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/requests/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6" }, ] [[package]] name = "rpds-py" version = "0.28.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, - { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, - { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, - { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, - { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, - { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, - { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, - { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, - { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, - { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, - { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, - { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, - { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, - { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rpds-py/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578" }, ] [[package]] name = "rsa" version = "4.9.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rsa/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/rsa/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762" }, ] [[package]] name = "shapely" version = "2.1.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, - { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, - { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, - { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, - { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, - { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, - { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, - { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, - { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, - { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, - { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, - { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, - { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, - { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, - { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, - { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, - { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, - { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shapely/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9" }, ] [[package]] name = "shellingham" version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shellingham/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/shellingham/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686" }, ] [[package]] name = "six" version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/six/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/six/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" }, ] [[package]] name = "sniffio" version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sniffio/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" }, ] [[package]] name = "sqlalchemy" version = "2.0.44" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, - { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, - { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05" }, ] [[package]] name = "sqlalchemy-spanner" version = "1.17.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "alembic" }, { name = "google-cloud-spanner" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/64/74e4d7aebc5210feff9b27e799fa81cc2bdf38f474e304e5c2b3f934f361/sqlalchemy_spanner-1.17.1.tar.gz", hash = "sha256:1542c2e69b1923974d8ad884ffc458f7d135e44af1c475b98decf75d90eccaa3", size = 82630, upload-time = "2025-10-21T14:33:54.183Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy-spanner/sqlalchemy_spanner-1.17.1.tar.gz", hash = "sha256:1542c2e69b1923974d8ad884ffc458f7d135e44af1c475b98decf75d90eccaa3" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/72/187ca1767648d54ada46c074b2b346894712bc56b6c0dab3410bd0996209/sqlalchemy_spanner-1.17.1-py3-none-any.whl", hash = "sha256:8b8444c23e66c84aab5dbab589face8fd75733fa6c1811db368d5202cdfb5f8e", size = 31859, upload-time = "2025-10-21T14:33:52.926Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sqlalchemy-spanner/sqlalchemy_spanner-1.17.1-py3-none-any.whl", hash = "sha256:8b8444c23e66c84aab5dbab589face8fd75733fa6c1811db368d5202cdfb5f8e" }, ] [[package]] @@ -2345,197 +2343,197 @@ wheels = [ [[package]] name = "sse-starlette" version = "3.0.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sse-starlette/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/sse-starlette/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431" }, ] [[package]] name = "starlette" version = "0.49.3" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/starlette/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f" }, ] [[package]] name = "tenacity" version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tenacity/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138" }, ] [[package]] name = "tiktoken" version = "0.12.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tiktoken/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71" }, ] [[package]] name = "tokenizers" version = "0.22.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tokenizers/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138" }, ] [[package]] name = "tqdm" version = "4.67.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tqdm/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tqdm/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2" }, ] [[package]] name = "typer-slim" version = "0.20.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "click" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561, upload-time = "2025-10-20T17:03:46.642Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typer-slim/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087, upload-time = "2025-10-20T17:03:44.546Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typer-slim/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d" }, ] [[package]] name = "typing-extensions" version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-extensions/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548" }, ] [[package]] name = "typing-inspection" version = "0.4.2" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/typing-inspection/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7" }, ] [[package]] name = "tzdata" version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzdata/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8" }, ] [[package]] name = "tzlocal" version = "5.3.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/tzlocal/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d" }, ] [[package]] name = "uritemplate" version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uritemplate/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uritemplate/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686" }, ] [[package]] name = "urllib3" version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/urllib3/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" }, ] [[package]] name = "uvicorn" version = "0.38.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/uvicorn/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02" }, ] [[package]] @@ -2546,127 +2544,127 @@ source = { virtual = "." } [[package]] name = "watchdog" version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/watchdog/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680" }, + { url = "https://us-python.pkg.dev/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 = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151" }, + { url = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/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 = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/websockets/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f" }, ] [[package]] name = "yarl" version = "1.22.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71" } +wheels = [ + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/yarl/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff" }, ] [[package]] name = "zipp" version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +source = { registry = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/simple/" } +sdist = { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/zipp/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://us-python.pkg.dev/artifact-foundry-prod/ah-3p-staging-python/zipp/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e" }, ] diff --git a/examples/verdure/server/verdure/__main__.py b/examples/verdure/server/verdure/__main__.py index 1c05c9789..5ddecfbfe 100644 --- a/examples/verdure/server/verdure/__main__.py +++ b/examples/verdure/server/verdure/__main__.py @@ -103,7 +103,10 @@ def main(host: str, port: int, base_url: str | None): allow_headers=["*"], ) - app.mount("/images", StaticFiles(directory="images"), name="images") + import pathlib + current_dir = pathlib.Path(__file__).parent.resolve() + images_dir = current_dir / "images" + app.mount("/images", StaticFiles(directory=images_dir), name="images") uvicorn.run(app, host=host, port=port) except MissingAPIKeyError as e: diff --git a/examples/verdure/server/verdure/a2ui_schema.py b/examples/verdure/server/verdure/a2ui_schema.py index 02f0de7a2..9e7329d4f 100644 --- a/examples/verdure/server/verdure/a2ui_schema.py +++ b/examples/verdure/server/verdure/a2ui_schema.py @@ -12,781 +12,1522 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + # This file serves as the single source of truth for the A2UI Schema. # It is imported by agent.py (for validation) and prompt_builder.py (for prompting). +# The schema is dynamically built from the three constituent JSON schemas below. -A2UI_SCHEMA = r""" +_SERVER_TO_CLIENT_JSON = r""" { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/server_to_client.json", "title": "A2UI Message Schema", - "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", + "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces.", "type": "object", - "properties": { - "beginRendering": { + "oneOf": [ + { "$ref": "#/$defs/CreateSurfaceMessage" }, + { "$ref": "#/$defs/UpdateComponentsMessage" }, + { "$ref": "#/$defs/UpdateDataModelMessage" }, + { "$ref": "#/$defs/DeleteSurfaceMessage" } + ], + "$defs": { + "CreateSurfaceMessage": { "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." + "version": { + "const": "v0.9" }, - "root": { - "type": "string", - "description": "The ID of the root component to render." + "createSurface": { + "type": "object", + "description": "Signals the client to create a new surface and begin rendering it. When this message is sent, the client will expect 'updateComponents' and/or 'updateDataModel' messages for the same surfaceId that define the component tree.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be rendered." + }, + "catalogId": { + "description": "A string that uniquely identifies this catalog. It is recommended to prefix this with an internet domain that you own, to avoid conflicts e.g. mycompany.com:somecatalog'.", + "type": "string" + }, + "theme": { + "type": "object", + "description": "Initial theme parameters for the surface (e.g., {'primaryColor': '#FF0000'}). These must validate against the 'theme' schema defined in the catalog.", + "additionalProperties": true + }, + "sendDataModel": { + "type": "boolean", + "description": "If true, the client will send the full data model of this surface in the metadata of every A2A message sent to the server that created the surface. Defaults to false." + } + }, + "required": ["surfaceId", "catalogId"], + "additionalProperties": false + } + }, + "required": ["createSurface", "version"], + "additionalProperties": false + }, + "UpdateComponentsMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "updateComponents": { + "type": "object", + "description": "Updates a surface with a new set of components. This message can be sent multiple times to update the component tree of an existing surface. One of the components in one of the components lists MUST have an 'id' of 'root' to serve as the root of the component tree. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be updated." + }, + + "components": { + "type": "array", + "description": "A list containing all UI components for the surface.", + "minItems": 1, + "items": { + "$ref": "catalog.json#/$defs/anyComponent" + } + } + }, + "required": ["surfaceId", "components"], + "additionalProperties": false + } + }, + "required": ["updateComponents", "version"], + "additionalProperties": false + }, + "UpdateDataModelMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" }, - "styles": { + "updateDataModel": { "type": "object", - "description": "Styling information for the UI.", + "description": "Updates the data model for an existing surface. This message can be sent multiple times to update the data model. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", "properties": { - "font": { + "surfaceId": { "type": "string", - "description": "The primary font for the UI." + "description": "The unique identifier for the UI surface this data model update applies to." }, - "primaryColor": { + "path": { "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" + "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', refers to the entire data model." + }, + "value": { + "description": "The data to be updated in the data model. If present, the value at 'path' is replaced (or created). If omitted, the key at 'path' is removed.", + "additionalProperties": true } - } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["updateDataModel", "version"], + "additionalProperties": false + }, + "DeleteSurfaceMessage": { + "type": "object", + "properties": { + "version": { + "const": "v0.9" + }, + "deleteSurface": { + "type": "object", + "description": "Signals the client to delete the surface identified by 'surfaceId'. The createSurface message MUST have been previously sent with the 'catalogId' that is in this message.", + "properties": { + "surfaceId": { + "type": "string", + "description": "The unique identifier for the UI surface to be deleted." + } + }, + "required": ["surfaceId"], + "additionalProperties": false + } + }, + "required": ["deleteSurface", "version"], + "additionalProperties": false + } + } +} +""" + +_COMMON_TYPES_JSON = r""" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/common_types.json", + "title": "A2UI Common Types", + "description": "Common type definitions used across A2UI schemas.", + "$defs": { + "ComponentId": { + "type": "string", + "description": "The unique identifier for a component, used for both definitions and references within the same surface." + }, + "AccessibilityAttributes": { + "type": "object", + "description": "Attributes to enhance accessibility when using assistive technologies like screen readers.", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + "description": "A short string, typically 1 to 3 words, used by assistive technologies to convey the purpose or intent of an element. For example, an input field might have an accessible label of 'User ID' or a button might be labeled 'Submit'." + }, + "description": { + "$ref": "#/$defs/DynamicString", + "description": "Additional information provided by assistive technologies about an element such as instructions, format requirements, or result of an action. For example, a mute button might have a label of 'Mute' and a description of 'Silences notifications about this conversation'." + } + } + }, + "ComponentCommon": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/ComponentId" + }, + "accessibility": { + "$ref": "#/$defs/AccessibilityAttributes" } }, - "required": ["root", "surfaceId"] + "required": ["id"] + }, + "ChildList": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/ComponentId" + }, + "description": "A static list of child component IDs." + }, + { + "type": "object", + "description": "A template for generating a dynamic list of children from a data model list. The `componentId` is the component to use as a template.", + "properties": { + "componentId": { + "$ref": "#/$defs/ComponentId" + }, + "path": { + "type": "string", + "description": "The path to the list of component property objects in the data model." + } + }, + "required": ["componentId", "path"], + "additionalProperties": false + } + ] }, - "surfaceUpdate": { + "DataBinding": { "type": "object", - "description": "Updates a surface with a new set of components.", "properties": { - "surfaceId": { + "path": { "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." + "description": "A JSON Pointer path to a value in the data model." + } + }, + "required": ["path"], + "additionalProperties": false + }, + "DynamicValue": { + "description": "A value that can be a literal, a path, or a function call returning any type.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/FunctionCall" + } + ] + }, + "DynamicString": { + "description": "Represents a string", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "string" + } + }, + "required": ["returnType"] + } + ] + } + ] + }, + "DynamicNumber": { + "description": "Represents a value that can be either a literal number, a path to a number in the data model, or a function call returning a number.", + "oneOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "number" + } + }, + "required": ["returnType"] + } + ] + } + ] + }, + "DynamicBoolean": { + "description": "A boolean value that can be a literal, a path, or a logic expression (including function calls returning boolean).", + "oneOf": [ + { + "type": "boolean" }, - "components": { + { + "$ref": "#/$defs/DataBinding" + }, + { + "$ref": "#/$defs/LogicExpression" + } + ] + }, + "DynamicStringList": { + "description": "Represents a value that can be either a literal array of strings, a path to a string array in the data model, or a function call returning a string array.", + "oneOf": [ + { "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." + "type": "string" + } + }, + { + "$ref": "#/$defs/DataBinding" + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "array" + } }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." + "required": ["returnType"] + } + ] + } + ] + }, + "FunctionCall": { + "type": "object", + "description": "Invokes a named function on the client.", + "properties": { + "call": { + "type": "string", + "description": "The name of the function to call." + }, + "args": { + "type": "object", + "description": "Arguments passed to the function.", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/DynamicValue" }, - "component": { + { "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Text'). The value is an object containing the properties for that specific component.", - "properties": { - "Text": { - "type": "object", - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "fit": { - "type": "string", - "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", - "enum": [ - "contain", - "cover", - "fill", - "none", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "properties": { - "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["name"] - }, - "Video": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": ["center", "end", "start", "stretch"] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } - }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "properties": { - "tabItems": { - "type": "array", - "description": "An array of objects, where each object defines a tab with a title and a child component.", - "items": { - "type": "object", - "properties": { - "title": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "child": { - "type": "string" - } - }, - "required": ["title", "child"] - } - } - }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "properties": { - "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } - } - }, - "Modal": { - "type": "object", - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "properties": { - "name": { - "type": "string" - }, - "context": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "properties": { - "path": { - "type": "string" - }, - "literalString": { - "type": "string" - }, - "literalNumber": { - "type": "number" - }, - "literalBoolean": { - "type": "boolean" - } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] + "description": "A literal object argument (e.g. configuration)." + } + ] + } + }, + "returnType": { + "type": "string", + "description": "The expected return type of the function call.", + "enum": [ + "string", + "number", + "boolean", + "array", + "object", + "any", + "void" + ], + "default": "boolean" + } + }, + "required": ["call"] + }, + "LogicExpression": { + "type": "object", + "description": "A boolean expression used for conditional state (e.g. 'enabled').", + "oneOf": [ + { + "properties": { + "and": { + "type": "array", + "items": { + "$ref": "#/$defs/LogicExpression" + }, + "minItems": 1 + } + }, + "required": ["and"] + }, + { + "properties": { + "or": { + "type": "array", + "items": { + "$ref": "#/$defs/LogicExpression" + }, + "minItems": 1 + } + }, + "required": ["or"] + }, + { + "properties": { + "not": { + "$ref": "#/$defs/LogicExpression" + } + }, + "required": ["not"] + }, + { + "allOf": [ + { + "$ref": "#/$defs/FunctionCall" + }, + { + "properties": { + "returnType": { + "const": "boolean" + } + } + } + ] + }, + { + "properties": { + "true": { + "const": true + } + }, + "required": ["true"] + }, + { + "properties": { + "false": { + "const": false + } + }, + "required": ["false"] + } + ] + }, + "CheckRule": { + "type": "object", + "description": "A single validation rule applied to an input component.", + "unevaluatedProperties": false, + "allOf": [ + { + "$ref": "#/$defs/LogicExpression" + }, + { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "The error message to display if the check fails." + } + }, + "required": ["message"] + } + ] + }, + "Checkable": { + "description": "Properties for components that support client-side checks.", + "type": "object", + "properties": { + "checks": { + "type": "array", + "description": "A list of checks to perform. These are function calls that must return a boolean indicating validity.", + "items": { + "$ref": "#/$defs/CheckRule" + } + } + } + }, + "Action": { + "description": "Defines an interaction handler that can either trigger a server-side event or execute a local client-side function.", + "oneOf": [ + { + "type": "object", + "description": "Triggers a server-side event.", + "properties": { + "event": { + "type": "object", + "description": "The event to dispatch to the server.", + "properties": { + "name": { + "type": "string", + "description": "The name of the action to be dispatched to the server." + }, + "context": { + "type": "object", + "description": "A JSON object containing the key-value pairs for the action context. Values can be literals or paths. Use literal values unless the value must be dynamically bound to the data model. Do NOT use paths for static IDs.", + "additionalProperties": { + "$ref": "#/$defs/DynamicValue" + } + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["event"], + "additionalProperties": false + }, + { + "type": "object", + "description": "Executes a local client-side function.", + "properties": { + "functionCall": { + "$ref": "#/$defs/FunctionCall" + } + }, + "required": ["functionCall"], + "additionalProperties": false + } + ] + } + } +} +""" + +_BASIC_CATALOG_JSON = r""" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://a2ui.org/specification/v0_9/standard_catalog.json", + "title": "A2UI Basic Catalog", + "description": "Unified catalog of basic A2UI components and functions.", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Text" }, + "text": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text content to display. While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation." + }, + "variant": { + "type": "string", + "description": "A hint for the base text style.", + "enum": ["h1", "h2", "h3", "h4", "h5", "caption", "body"] + } + }, + "required": ["component", "text"] + } + ], + "unevaluatedProperties": false + }, + "Image": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Image" }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the image to display." + }, + "fit": { + "type": "string", + "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", + "enum": ["contain", "cover", "fill", "none", "scale-down"] + }, + "variant": { + "type": "string", + "description": "A hint for the image size and style.", + "enum": [ + "icon", + "avatar", + "smallFeature", + "mediumFeature", + "largeFeature", + "header" + ] + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Icon": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Icon" }, + "name": { + "description": "The name of the icon to display.", + "oneOf": [ + { + "type": "string", + "enum": [ + "accountCircle", + "add", + "arrowBack", + "arrowForward", + "attachFile", + "calendarToday", + "call", + "camera", + "check", + "close", + "delete", + "download", + "edit", + "event", + "error", + "fastForward", + "favorite", + "favoriteOff", + "folder", + "help", + "home", + "info", + "locationOn", + "lock", + "lockOpen", + "mail", + "menu", + "moreVert", + "moreHoriz", + "notificationsOff", + "notifications", + "pause", + "payment", + "person", + "phone", + "photo", + "play", + "print", + "refresh", + "rewind", + "search", + "send", + "settings", + "share", + "shoppingCart", + "skipNext", + "skipPrevious", + "star", + "starHalf", + "starOff", + "stop", + "upload", + "visibility", + "visibilityOff", + "volumeDown", + "volumeMute", + "volumeOff", + "volumeUp", + "warning" + ] + }, + { + "type": "object", + "properties": { + "path": { "type": "string" } }, - "CheckBox": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "properties": { - "literalBoolean": { - "type": "boolean" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["label", "value"] + "required": ["path"], + "additionalProperties": false + } + ] + } + }, + "required": ["component", "name"] + } + ], + "unevaluatedProperties": false + }, + "Video": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Video" }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the video to display." + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "AudioPlayer": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "AudioPlayer" }, + "url": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The URL of the audio to be played." + }, + "description": { + "description": "A description of the audio, such as a title or summary.", + "$ref": "common_types.json#/$defs/DynamicString" + } + }, + "required": ["component", "url"] + } + ], + "unevaluatedProperties": false + }, + "Row": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "description": "A layout component that arranges its children horizontally. To create a grid layout, nest Columns within this Row.", + "properties": { + "component": { "const": "Row" }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (horizontally). Use 'spaceBetween' to push items to the edges, or 'start'/'end'/'center' to pack them together.", + "enum": [ + "center", + "end", + "spaceAround", + "spaceBetween", + "spaceEvenly", + "start", + "stretch" + ] + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (vertically). This is similar to the CSS 'align-items' property, but uses camelCase values (e.g., 'start').", + "enum": ["start", "center", "end", "stretch"] + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Column": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "description": "A layout component that arranges its children vertically. To create a grid layout, nest Rows within this Column.", + "properties": { + "component": { "const": "Column" }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list. Children cannot be defined inline, they must be referred to by ID.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "justify": { + "type": "string", + "description": "Defines the arrangement of children along the main axis (vertically). Use 'spaceBetween' to push items to the edges (e.g. header at top, footer at bottom), or 'start'/'end'/'center' to pack them together.", + "enum": [ + "start", + "center", + "end", + "spaceBetween", + "spaceAround", + "spaceEvenly", + "stretch" + ] + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis (horizontally). This is similar to the CSS 'align-items' property.", + "enum": ["center", "end", "start", "stretch"] + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "List": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "List" }, + "children": { + "description": "Defines the children. Use an array of strings for a fixed set of children, or a template object to generate children from a data list.", + "$ref": "common_types.json#/$defs/ChildList" + }, + "direction": { + "type": "string", + "description": "The direction in which the list items are laid out.", + "enum": ["vertical", "horizontal"] + }, + "align": { + "type": "string", + "description": "Defines the alignment of children along the cross axis.", + "enum": ["start", "center", "end", "stretch"] + } + }, + "required": ["component", "children"] + } + ], + "unevaluatedProperties": false + }, + "Card": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Card" }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the single child component to be rendered inside the card. To display multiple elements, you MUST wrap them in a layout component (like Column or Row) and pass that container's ID here. Do NOT pass multiple IDs or a non-existent ID. Do NOT define the child component inline." + } + }, + "required": ["component", "child"] + } + ], + "unevaluatedProperties": false + }, + "Tabs": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Tabs" }, + "tabs": { + "type": "array", + "description": "An array of objects, where each object defines a tab with a title and a child component.", + "items": { + "type": "object", + "properties": { + "title": { + "description": "The tab title.", + "$ref": "common_types.json#/$defs/DynamicString" }, - "TextField": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Do NOT define the component inline." + } + }, + "required": ["title", "child"], + "additionalProperties": false + } + } + }, + "required": ["component", "tabs"] + } + ], + "unevaluatedProperties": false + }, + "Modal": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Modal" }, + "trigger": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component that opens the modal when interacted with (e.g., a button). Do NOT define the component inline." + }, + "content": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the component to be displayed inside the modal. Do NOT define the component inline." + } + }, + "required": ["component", "trigger", "content"] + } + ], + "unevaluatedProperties": false + }, + "Divider": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { + "type": "object", + "properties": { + "component": { "const": "Divider" }, + "axis": { + "type": "string", + "description": "The orientation of the divider.", + "enum": ["horizontal", "vertical"], + "default": "horizontal" + } + }, + "required": ["component"] + } + ], + "unevaluatedProperties": false + }, + "Button": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "Button" }, + "child": { + "$ref": "common_types.json#/$defs/ComponentId", + "description": "The ID of the child component. Use a 'Text' component for a labeled button. Only use an 'Icon' if the requirements explicitly ask for an icon-only button. Do NOT define the child component inline." + }, + "variant": { + "type": "string", + "description": "A hint for the button style. If omitted, a default button style is used. 'primary' indicates this is the main call-to-action button. 'borderless' means the button has no visual border or background, making its child content appear like a clickable link.", + "enum": ["primary", "borderless"] + }, + "action": { + "$ref": "common_types.json#/$defs/Action" + } + }, + "required": ["component", "child", "action"] + } + ], + "unevaluatedProperties": false + }, + "TextField": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "TextField" }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The value of the text field." + }, + "variant": { + "type": "string", + "description": "The type of input field to display.", + "enum": ["longText", "number", "shortText", "obscured"] + } + }, + "required": ["component", "label"] + } + ], + "unevaluatedProperties": false + }, + "CheckBox": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "CheckBox" }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text to display next to the checkbox." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "The current state of the checkbox (true for checked, false for unchecked)." + } + }, + "required": ["component", "label", "value"] + } + ], + "unevaluatedProperties": false + }, + "ChoicePicker": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "description": "A component that allows selecting one or more options from a list.", + "properties": { + "component": { "const": "ChoicePicker" }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the group of options." + }, + "variant": { + "type": "string", + "description": "A hint for how the choice picker should be displayed and behave.", + "enum": ["multipleSelection", "mutuallyExclusive"] + }, + "options": { + "type": "array", + "description": "The list of available options to choose from.", + "items": { + "type": "object", + "properties": { + "label": { + "description": "The text to display for this option.", + "$ref": "common_types.json#/$defs/DynamicString" }, - "DateTimeInput": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - }, - "outputFormat": { - "type": "string", - "description": "The desired format for the output string after a date or time is selected." - } - }, - "required": ["value"] + "value": { + "type": "string", + "description": "The stable value associated with this option." + } + }, + "required": ["label", "value"], + "additionalProperties": false + } + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicStringList", + "description": "The list of currently selected values. This should be bound to a string array in the data model." + } + }, + "required": ["component", "options", "value"] + } + ], + "unevaluatedProperties": false + }, + "Slider": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "Slider" }, + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The label for the slider." + }, + "min": { + "type": "number", + "description": "The minimum value of the slider." + }, + "max": { + "type": "number", + "description": "The maximum value of the slider." + }, + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The current value of the slider." + } + }, + "required": ["component", "value", "min", "max"] + } + ], + "unevaluatedProperties": false + }, + "DateTimeInput": { + "type": "object", + "allOf": [ + { "$ref": "common_types.json#/$defs/ComponentCommon" }, + { "$ref": "#/$defs/CatalogComponentCommon" }, + { "$ref": "common_types.json#/$defs/Checkable" }, + { + "type": "object", + "properties": { + "component": { "const": "DateTimeInput" }, + "value": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The selected date and/or time value in ISO 8601 format. If not yet set, initialize with an empty string." + }, + "enableDate": { + "type": "boolean", + "description": "If true, allows the user to select a date." + }, + "enableTime": { + "type": "boolean", + "description": "If true, allows the user to select a time." + }, + "min": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" }, - "MultipleChoice": { - "type": "object", - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "properties": { - "literalArray": { - "type": "array", - "items": { - "type": "string" - } - }, - "path": { - "type": "string" - } - } + "then": { + "oneOf": [ + { + "format": "date" }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } + { + "format": "time" }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." + { + "format": "date-time" } - }, - "required": ["selections", "options"] + ] + } + } + ], + "description": "The minimum allowed date/time in ISO 8601 format." + }, + "max": { + "allOf": [ + { + "$ref": "common_types.json#/$defs/DynamicString" + }, + { + "if": { + "type": "string" }, - "Slider": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "properties": { - "literalNumber": { - "type": "number" - }, - "path": { - "type": "string" - } - } + "then": { + "oneOf": [ + { + "format": "date" }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." + { + "format": "time" }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." + { + "format": "date-time" } - }, - "required": ["value"] + ] } } - } + ], + "description": "The maximum allowed date/time in ISO 8601 format." }, - "required": ["id", "component"] - } + "label": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The text label for the input field." + } + }, + "required": ["component", "value"] } - }, - "required": ["surfaceId", "components"] + ], + "unevaluatedProperties": false + } + }, + "functions": [ + { + "name": "required", + "description": "Checks that the value is not null, undefined, or empty.", + "returnType": "boolean", + "parameters": { + "type": "object", + "properties": { + "value": { + "description": "The value to check." + } + }, + "required": ["value"], + "unevaluatedProperties": false + } }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." + { + "name": "regex", + "description": "Checks that the value matches a regular expression string.", + "returnType": "boolean", + "parameters": { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "pattern": { + "type": "string", + "description": "The regex pattern to match against." + } }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." + "required": ["value", "pattern"], + "unevaluatedProperties": false + } + }, + { + "name": "length", + "description": "Checks string length constraints.", + "returnType": "boolean", + "parameters": { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" }, + "min": { + "type": "integer", + "minimum": 0, + "description": "The minimum allowed length." + }, + "max": { + "type": "integer", + "minimum": 0, + "description": "The maximum allowed length." + } }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], + "unevaluatedProperties": false + } + }, + { + "name": "numeric", + "description": "Checks numeric range constraints.", + "returnType": "boolean", + "parameters": { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicNumber" }, + "min": { + "type": "number", + "description": "The minimum allowed value." + }, + "max": { + "type": "number", + "description": "The maximum allowed value." + } + }, + "required": ["value"], + "anyOf": [{ "required": ["min"] }, { "required": ["max"] }], + "unevaluatedProperties": false + } + }, + { + "name": "email", + "description": "Checks that the value is a valid email address.", + "returnType": "boolean", + "parameters": { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], + "unevaluatedProperties": false + } + }, + { + "name": "formatString", + "description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${data.path}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be named (e.g., `${formatDate(value:${/currentDate}, format:'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.", + "returnType": "string", + "parameters": { + "type": "object", + "properties": { + "value": { "$ref": "common_types.json#/$defs/DynamicString" } + }, + "required": ["value"], + "unevaluatedProperties": false + } + }, + { + "name": "formatNumber", + "description": "Formats a number with the specified grouping and decimal precision.", + "returnType": "string", + "parameters": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The number to format." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["value"], + "unevaluatedProperties": false + } + }, + { + "name": "formatCurrency", + "description": "Formats a number as a currency string.", + "returnType": "string", + "parameters": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The monetary amount." + }, + "currency": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The ISO 4217 currency code (e.g., 'USD', 'EUR')." + }, + "decimals": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale." + }, + "grouping": { + "$ref": "common_types.json#/$defs/DynamicBoolean", + "description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true." + } + }, + "required": ["currency", "value"], + "unevaluatedProperties": false + } + }, + { + "name": "formatDate", + "description": "Formats a timestamp into a string using a pattern.", + "returnType": "string", + "parameters": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicValue", + "description": "The date to format." + }, + "format": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'" + } + }, + "required": ["format", "value"], + "unevaluatedProperties": false + } + }, + { + "name": "pluralize", + "description": "Returns a localized string based on the Common Locale Data Repository (CLDR) plural category of the count (zero, one, two, few, many, other). Requires an 'other' fallback. For English, just use 'one' and 'other'.", + "returnType": "string", + "parameters": { + "type": "object", + "properties": { + "value": { + "$ref": "common_types.json#/$defs/DynamicNumber", + "description": "The numeric value used to determine the plural category." + }, + "zero": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'zero' category (e.g., 0 items)." + }, + "one": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'one' category (e.g., 1 item)." + }, + "two": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'two' category (used in Arabic, Welsh, etc.)." + }, + "few": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'few' category (e.g., small groups in Slavic languages)." + }, + "many": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "String for the 'many' category (e.g., large groups in various languages)." + }, + "other": { + "$ref": "common_types.json#/$defs/DynamicString", + "description": "The default/fallback string (used for general plural cases)." + } + }, + "required": ["value", "other"], + "unevaluatedProperties": false + } + }, + { + "name": "openUrl", + "description": "Opens the specified URL in a browser or handler. This function has no return value.", + "returnType": "void", + "parameters": { + "allOf": [ + { "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", "properties": { - "key": { + "url": { "type": "string", - "description": "The key for this data entry." - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string" - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - } - }, - "required": ["key"] - } + "format": "uri", + "description": "The URL to open." } }, - "required": ["key"] + "required": ["url"] } - } - }, - "required": ["contents", "surfaceId"] + ], + "unevaluatedProperties": false + } + } + ], + "theme": { + "primaryColor": { + "type": "string", + "description": "The primary brand color used for highlights (e.g., primary buttons, active borders). Renderers may generate variants of this color for different contexts. Format: Hexadecimal code (e.g., '#00BFFF').", + "pattern": "^#[0-9a-fA-F]{6}$" }, - "deleteSurface": { + "iconUrl": { + "type": "string", + "format": "uri", + "description": "A URL for an image that identifies the agent or tool associated with the surface." + }, + "agentDisplayName": { + "type": "string", + "description": "Text to be displayed next to the surface to identify the agent or tool that created it." + } + }, + "$defs": { + "CatalogComponentCommon": { "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." + "weight": { + "type": "number", + "description": "The relative weight of this component within a Row or Column. This is similar to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." } - }, - "required": ["surfaceId"] + } + }, + "anyComponent": { + "oneOf": [ + { "$ref": "#/components/Text" }, + { "$ref": "#/components/Image" }, + { "$ref": "#/components/Icon" }, + { "$ref": "#/components/Video" }, + { "$ref": "#/components/AudioPlayer" }, + { "$ref": "#/components/Row" }, + { "$ref": "#/components/Column" }, + { "$ref": "#/components/List" }, + { "$ref": "#/components/Card" }, + { "$ref": "#/components/Tabs" }, + { "$ref": "#/components/Modal" }, + { "$ref": "#/components/Divider" }, + { "$ref": "#/components/Button" }, + { "$ref": "#/components/TextField" }, + { "$ref": "#/components/CheckBox" }, + { "$ref": "#/components/ChoicePicker" }, + { "$ref": "#/components/Slider" }, + { "$ref": "#/components/DateTimeInput" } + ], + "discriminator": { + "propertyName": "component" + } } } } """ + +def _build_unified_schema(): + server_schema = json.loads(_SERVER_TO_CLIENT_JSON) + common_schema = json.loads(_COMMON_TYPES_JSON) + catalog_schema = json.loads(_BASIC_CATALOG_JSON) + + # Initialize $defs if not present + if "$defs" not in server_schema: + server_schema["$defs"] = {} + + # 1. Merge common types $defs + if "$defs" in common_schema: + server_schema["$defs"].update(common_schema["$defs"]) + + # 2. Merge catalog $defs (e.g. CatalogComponentCommon, anyComponent) + if "$defs" in catalog_schema: + server_schema["$defs"].update(catalog_schema["$defs"]) + + # 3. Add catalog definitions (components, functions, theme) to appropriate places. + # The unified schema should ideally look like it has all definitions. + # "anyComponent" refers to "#/components/Text", so we need "components" at root. + if "components" in catalog_schema: + server_schema["components"] = catalog_schema["components"] + + # We might also want "functions" and "theme" if there are any refs or just for completeness. + # Though validation primarily checks against "oneOf" messages. + if "functions" in catalog_schema: + server_schema["functions"] = catalog_schema["functions"] + if "theme" in catalog_schema: + server_schema["theme"] = catalog_schema["theme"] + + # 4. Rewrite References + # Recursively traverse and replace: + # "common_types.json#" -> "#" + # "catalog.json#" -> "#" + + def _rewrite_refs(obj): + if isinstance(obj, dict): + for k, v in obj.items(): + if k == "$ref" and isinstance(v, str): + v = v.replace("common_types.json#", "#") + v = v.replace("catalog.json#", "#") + # Also handle if standard_catalog self-refs using just relative paths if any, + # but usually they are #/$defs or json file refs. + obj[k] = v + else: + _rewrite_refs(v) + elif isinstance(obj, list): + for item in obj: + _rewrite_refs(item) + + _rewrite_refs(server_schema) + + return json.dumps(server_schema, indent=2) + +A2UI_SCHEMA = _build_unified_schema() diff --git a/examples/verdure/server/verdure/agent.py b/examples/verdure/server/verdure/agent.py index c72a1d010..23ca3fc91 100644 --- a/examples/verdure/server/verdure/agent.py +++ b/examples/verdure/server/verdure/agent.py @@ -60,7 +60,7 @@ d. If a user uploads a photo of a grassy lawn, your questions should be about *that* (e.g., "Add a flower bed?", "Install a patio?"). 4. **Get Options:** - a. When you receive a query like 'USER_SUBMITTED_QUESTIONNAIRE...', you MUST first call the `get_landscape_options` tool. Extract the budget, style, maintenance, and space description from the query. + a. When you receive a query like 'USER_SUBMITTED_QUESTIONNAIRE...', you MUST first call the `get_landscape_options` tool. Pass the `guest_count`, `preserve_bushes`, and `patio_plan` (or `lawn_plan`) from the query to the tool. b. After receiving the data, you MUST use the `OPTIONS_PRESENTATION_EXAMPLE` template, populating the `dataModelUpdate.contents` with the JSON data from the tool. 5. **Shopping Cart:** diff --git a/examples/verdure/server/verdure/agent_executor.py b/examples/verdure/server/verdure/agent_executor.py index fc852c440..cc77b2081 100644 --- a/examples/verdure/server/verdure/agent_executor.py +++ b/examples/verdure/server/verdure/agent_executor.py @@ -88,9 +88,9 @@ async def execute( ) for i, part in enumerate(context.message.parts): if isinstance(part.root, DataPart): - if "userAction" in part.root.data: + if "action" in part.root.data: logger.info(f" Part {i}: Found a2ui UI ClientEvent payload.") - ui_event_part = part.root.data["userAction"] + ui_event_part = part.root.data["action"] else: logger.info(f" Part {i}: DataPart (data: {part.root.data})") elif isinstance(part.root, TextPart): @@ -100,29 +100,45 @@ async def execute( file_data = part.root.file if file_data.bytes: logger.info(f" Extracting {len(part.root.file.bytes)} bytes") - try: - image_bytes = base64.b64decode(file_data.bytes) - mime_type = file_data.mime_type - extension = { - "image/png": ".png", - "image/jpeg": ".jpg", - "image/heic": ".heic", - "image/webp": ".webp", - }.get(mime_type, ".jpg") - filename = f"{uuid.uuid4()}{extension}" - images_dir = os.path.join(os.path.dirname(__file__), "images", "uploads") - os.makedirs(images_dir, exist_ok=True) - filepath = os.path.join(images_dir, filename) - with open(filepath, "wb") as f: - f.write(image_bytes) - - image_url = f"{self.ui_agent.base_url}/images/uploads/{filename}" - mime_type = file_data.mime_type if file_data.mime_type else "image/jpeg" - image_part = ImagePart(image_url, mime_type, image_bytes) - logger.info(f" Part {i}: Set image_part to a {mime_type} image.") - logger.info(f" Saved FilePart to {filepath}, URL: {image_url}") - except Exception as e: - logger.error(f"Failed to save FilePart: {e}") + mime_type = file_data.mime_type or "" + logger.info(f" Part {i}: FilePart mime_type: '{mime_type}'") + if mime_type.startswith("application/json"): + logger.info(f" Part {i}: Found application/json FilePart. Parsing as UI event.") + try: + json_bytes = base64.b64decode(file_data.bytes) + json_str = json_bytes.decode("utf-8") + json_data = json.loads(json_str) + if "action" in json_data: + logger.info(f" Part {i}: Found a2ui UI ClientEvent payload in FilePart.") + ui_event_part = json_data["action"] + else: + logger.info(f" Part {i}: JSON FilePart (data: {json_data})") + except Exception as e: + logger.error(f"Failed to parse application/json FilePart: {e}") + else: + try: + image_bytes = base64.b64decode(file_data.bytes) + mime_type = file_data.mime_type + extension = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/heic": ".heic", + "image/webp": ".webp", + }.get(mime_type, ".jpg") + filename = f"{uuid.uuid4()}{extension}" + images_dir = os.path.join(os.path.dirname(__file__), "images", "uploads") + os.makedirs(images_dir, exist_ok=True) + filepath = os.path.join(images_dir, filename) + with open(filepath, "wb") as f: + f.write(image_bytes) + + image_url = f"{self.ui_agent.base_url}/images/uploads/{filename}" + mime_type = file_data.mime_type if file_data.mime_type else "image/jpeg" + image_part = ImagePart(image_url, mime_type, image_bytes) + logger.info(f" Part {i}: Set image_part to a {mime_type} image.") + logger.info(f" Saved FilePart to {filepath}, URL: {image_url}") + except Exception as e: + logger.error(f"Failed to save FilePart: {e}") elif file_data.uri: logger.info(f" Part {i}: FilePart has URI: {file_data.uri}") # Handle URI if needed, but for now focus on bytes diff --git a/examples/verdure/server/verdure/tools.py b/examples/verdure/server/verdure/tools.py index 0fc722721..c840a63fc 100644 --- a/examples/verdure/server/verdure/tools.py +++ b/examples/verdure/server/verdure/tools.py @@ -19,7 +19,13 @@ def get_landscape_options( - budget: str, style: str, maintenance: str, space_description: str + budget: str = "Unknown", + style: str = "Unknown", + maintenance: str = "Unknown", + space_description: str = "Unknown", + guest_count: int = 4, + preserve_bushes: bool = True, + lawn_plan: list[str] = [], ) -> str: """ Call this tool to get landscape design options based on user preferences. @@ -27,16 +33,22 @@ def get_landscape_options( 'style' is the desired landscape vibe (e.g., 'Modern', 'Zen', 'Cottage'). 'maintenance' is the preferred level (e.g., 'Low', 'Medium', 'High'). 'space_description' is the user's text description of their yard. + 'guest_count' is the number of people for entertaining. + 'preserve_bushes' whether to keep existing bushes. + 'lawn_plan' is the plan for the patio/lawn area. """ logger.info("--- TOOL CALLED: get_landscape_options ---") logger.info(f" - Budget: {budget}") logger.info(f" - Style: {style}") logger.info(f" - Maintenance: {maintenance}") logger.info(f" - Space: {space_description}") + logger.info(f" - Guest Count: {guest_count}") + logger.info(f" - Preserve Bushes: {preserve_bushes}") + logger.info(f" - Lawn Plan: {lawn_plan}") # In a real app, this would query a model or database. # Here, we return hardcoded options. - options = [ + items = [ { "name": "Modern Zen Garden", "detail": "Low maintenance, drought-tolerant plants, and clean lines. Perfect for relaxation.", @@ -57,10 +69,5 @@ def get_landscape_options( }, ] - # --- MODIFICATION --- - # Remove the filtering logic to always return both options - items = options - # --- END MODIFICATION --- - logger.info(f" - Success: Returning {len(items)} landscape options.") return json.dumps(items) diff --git a/examples/verdure/server/verdure/ui_examples.py b/examples/verdure/server/verdure/ui_examples.py index 19c5e1734..bc34445c6 100644 --- a/examples/verdure/server/verdure/ui_examples.py +++ b/examples/verdure/server/verdure/ui_examples.py @@ -18,25 +18,25 @@ LANDSCAPE_UI_EXAMPLES = """ ---BEGIN WELCOME_SCREEN_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "welcome", "root": "welcome-column", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "welcome", "catalogId": "https://a2ui.org/specification/v0.9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "welcome", "components": [ - {{ "id": "welcome-column", "component": {{ "Column": {{ "alignment": "center", "distribution": "center", "children": {{ "explicitList": ["logo-image", "welcome-title", "welcome-subtitle", "button-row"] }} }} }} }}, - {{ "id": "logo-image", "component": {{ "Image": {{ "url": {{ "literalString": "{base_url}/images/verdure_logo.png" }}, "fit": "contain" }} }} }}, - {{ "id": "welcome-title", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "literalString": "Envision Your Dream Landscape" }} }} }} }}, - {{ "id": "welcome-subtitle", "component": {{ "Text": {{ "text": {{ "literalString": "Bring your perfect outdoor space to life with our AI-powered design tools." }} }} }} }}, + {{ "id": "root", "component": "Column", "align": "center", "justify": "center", "children": ["logo-image", "welcome-title", "welcome-subtitle", "button-row"] }}, + {{ "id": "logo-image", "component": "Image", "url": "{base_url}/images/verdure_logo.png", "fit": "contain" }}, + {{ "id": "welcome-title", "component": "Text", "variant": "h1", "text": "Envision Your Dream Landscape" }}, + {{ "id": "welcome-subtitle", "component": "Text", "text": "Bring your perfect outdoor space to life with our AI-powered design tools." }}, - {{ "id": "button-row", "component": {{ "Row": {{ "distribution": "spaceEvenly", "alignment": "center", "children": {{ "explicitList": ["start-button", "explore-button", "returning-user-button"] }} }} }} }}, + {{ "id": "button-row", "component": "Row", "justify": "spaceEvenly", "align": "center", "children": ["start-button", "explore-button", "returning-user-button"] }}, - {{ "id": "start-button", "component": {{ "Button": {{ "child": "start-button-text", "primary": true, "action": {{ "name": "start_project" }} }} }} }}, - {{ "id": "start-button-text", "component": {{ "Text": {{ "text": {{ "literalString": "Start New Project" }} }} }} }}, + {{ "id": "start-button", "component": "Button", "child": "start-button-text", "variant": "primary", "action": {{ "event": {{ "name": "start_project" }} }} }}, + {{ "id": "start-button-text", "component": "Text", "text": "Start New Project" }}, - {{ "id": "explore-button", "component": {{ "Button": {{ "child": "explore-button-text", "primary": false, "action": {{ "name": "explore_ideas" }} }} }} }}, - {{ "id": "explore-button-text", "component": {{ "Text": {{ "text": {{ "literalString": "Explore Ideas" }} }} }} }}, + {{ "id": "explore-button", "component": "Button", "child": "explore-button-text", "action": {{ "event": {{ "name": "explore_ideas" }} }} }}, + {{ "id": "explore-button-text", "component": "Text", "text": "Explore Ideas" }}, - {{ "id": "returning-user-button", "component": {{ "Button": {{ "child": "returning-user-text", "primary": false, "action": {{ "name": "returning_user" }} }} }} }}, - {{ "id": "returning-user-text", "component": {{ "Text": {{ "text": {{ "literalString": "I'm a returning user" }} }} }} }} + {{ "id": "returning-user-button", "component": "Button", "child": "returning-user-text", "action": {{ "event": {{ "name": "returning_user" }} }} }}, + {{ "id": "returning-user-text", "component": "Text", "text": "I'm a returning user" }} ] }} }} ] @@ -44,11 +44,11 @@ ---BEGIN PROJECT_DETAILS_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "details", "root": "details-column", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "details", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "details", "components": [ - {{ "id": "details-column", "component": {{ "Column": {{ "alignment": "stretch", "children": {{ "explicitList": [ + {{ "id": "root", "component": "Column", "align": "stretch", "children": [ "header-row", "hero-image", "transformation-title", @@ -57,40 +57,40 @@ "choose-library-card", "tips-row", "upload-photo-button" - ] }} }} }} }}, + ] }}, - {{ "id": "header-row", "component": {{ "Row": {{ "distribution": "start", "alignment": "center", "children": {{ "explicitList": ["back-arrow", "header-title"] }} }} }} }}, - {{ "id": "back-arrow", "component": {{ "Icon": {{ "name": {{ "literalString": "arrow-back" }} }} }} }}, - {{ "id": "header-title", "component": {{ "Text": {{ "usageHint": "h3", "text": {{ "literalString": "Visualize Your Garden" }} }} }} }}, + {{ "id": "header-row", "component": "Row", "justify": "start", "align": "center", "children": ["back-arrow", "header-title"] }}, + {{ "id": "back-arrow", "component": "Icon", "name": "arrowBack" }}, + {{ "id": "header-title", "component": "Text", "variant": "h3", "text": "Visualize Your Garden" }}, - {{ "id": "hero-image", "component": {{ "Image": {{ "url": {{ "literalString": "{base_url}/images/header_image.png" }}, "fit": "cover" }} }} }}, + {{ "id": "hero-image", "component": "Image", "url": "{base_url}/images/header_image.png", "fit": "cover" }}, - {{ "id": "transformation-title", "component": {{ "Text": {{ "usageHint": "h1", "text": {{ "literalString": "Let's Start Your Transformation" }} }} }} }}, - {{ "id": "transformation-subtitle", "component": {{ "Text": {{ "text": {{ "literalString": "Upload a photo of your front or back yard, and our designers will use it to create a custom vision. Get ready to see the potential." }} }} }} }}, + {{ "id": "transformation-title", "component": "Text", "variant": "h1", "text": "Let's Start Your Transformation" }}, + {{ "id": "transformation-subtitle", "component": "Text", "text": "Upload a photo of your front or back yard, and our designers will use it to create a custom vision. Get ready to see the potential." }}, - {{ "id": "take-photo-card", "component": {{ "Card": {{ "child": "take-photo-row" }} }} }}, - {{ "id": "take-photo-row", "component": {{ "Row": {{ "distribution": "start", "alignment": "center", "children": {{ "explicitList": ["take-photo-icon", "take-photo-column"] }} }} }} }}, - {{ "id": "take-photo-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "camera-alt" }} }} }} }}, - {{ "id": "take-photo-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["take-photo-title", "take-photo-subtitle"] }} }} }} }}, - {{ "id": "take-photo-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "literalString": "Take a Photo" }} }} }} }}, - {{ "id": "take-photo-subtitle", "component": {{ "Text": {{ "text": {{ "literalString": "Capture your space directly from the app." }} }} }} }}, + {{ "id": "take-photo-card", "component": "Card", "child": "take-photo-row" }}, + {{ "id": "take-photo-row", "component": "Row", "justify": "start", "align": "center", "children": ["take-photo-icon", "take-photo-column"] }}, + {{ "id": "take-photo-icon", "component": "Icon", "name": "camera" }}, + {{ "id": "take-photo-column", "component": "Column", "children": ["take-photo-title", "take-photo-subtitle"] }}, + {{ "id": "take-photo-title", "component": "Text", "variant": "h4", "text": "Take a Photo" }}, + {{ "id": "take-photo-subtitle", "component": "Text", "text": "Capture your space directly from the app." }}, - {{ "id": "choose-library-card", "component": {{ "Card": {{ "child": "choose-library-row" }} }} }}, - {{ "id": "choose-library-row", "component": {{ "Row": {{ "distribution": "start", "alignment": "center", "children": {{ "explicitList": ["choose-library-icon", "choose-library-column"] }} }} }} }}, - {{ "id": "choose-library-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "photo-library" }} }} }} }}, - {{ "id": "choose-library-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["choose-library-title", "choose-library-subtitle"] }} }} }} }}, - {{ "id": "choose-library-title", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "literalString": "Choose from Library" }} }} }} }}, - {{ "id": "choose-library-subtitle", "component": {{ "Text": {{ "text": {{ "literalString": "Select a photo from your phone's gallery." }} }} }} }}, + {{ "id": "choose-library-card", "component": "Card", "child": "choose-library-row" }}, + {{ "id": "choose-library-row", "component": "Row", "justify": "start", "align": "center", "children": ["choose-library-icon", "choose-library-column"] }}, + {{ "id": "choose-library-icon", "component": "Icon", "name": "photo" }}, + {{ "id": "choose-library-column", "component": "Column", "children": ["choose-library-title", "choose-library-subtitle"] }}, + {{ "id": "choose-library-title", "component": "Text", "variant": "h4", "text": "Choose from Library" }}, + {{ "id": "choose-library-subtitle", "component": "Text", "text": "Select a photo from your phone's gallery." }}, - {{ "id": "tips-row", "component": {{ "Row": {{ "distribution": "center", "alignment": "center", "children": {{ "explicitList": ["tips-icon", "tips-text"] }} }} }} }}, - {{ "id": "tips-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "lightbulb" }} }} }} }}, - {{ "id": "tips-text", "component": {{ "Text": {{ "text": {{ "literalString": "Tips for the best photo" }} }} }} }}, + {{ "id": "tips-row", "component": "Row", "justify": "center", "align": "center", "children": ["tips-icon", "tips-text"] }}, + {{ "id": "tips-icon", "component": "Icon", "name": "info" }}, + {{ "id": "tips-text", "component": "Text", "text": "Tips for the best photo" }}, - {{ "id": "upload-photo-button", "component": {{ "Button": {{ "child": "upload-photo-text", "primary": true, "action": {{ "name": "submit_details", "context": [ - {{ "key": "yardDescription", "value": {{ "literalString": "Photo of an old backyard with a concrete patio and weeds." }} }}, - {{ "key": "imageUrl", "value": {{ "literalString": "{base_url}/images/old_backyard.png" }} }} - ] }} }} }} }}, - {{ "id": "upload-photo-text", "component": {{ "Text": {{ "text": {{ "literalString": "Upload Your Photo" }} }} }} }} + {{ "id": "upload-photo-button", "component": "Button", "child": "upload-photo-text", "variant": "primary", "action": {{ "event": {{ "name": "submit_details", "context": {{ + "yardDescription": "Photo of an old backyard with a concrete patio and weeds.", + "imageUrl": "{base_url}/images/old_backyard.png" + }} }} }} }}, + {{ "id": "upload-photo-text", "component": "Text", "text": "Upload Your Photo" }} ] }} }} ] @@ -98,11 +98,11 @@ ---BEGIN QUESTIONNAIRE_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "questionnaire", "root": "question-column", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "questionnaire", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "questionnaire", "components": [ - {{ "id": "question-column", "component": {{ "Column":{{ "alignment": "stretch", "children": {{ "explicitList": [ + {{ "id": "root", "component": "Column", "align": "stretch", "children": [ "user-photo", "q-entertain-slider-title", "q-entertain-slider", @@ -110,176 +110,178 @@ "q-patio-title", "q-patio-options", "q-submit-button" - ] }} }} }} }}, + ] }}, - {{ "id": "user-photo", "component": {{ "Image": {{ "url": {{ "path": "imageUrl" }}, "fit": "cover" }} }} }}, + {{ "id": "user-photo", "component": "Image", "url": {{ "path": "imageUrl" }}, "fit": "cover" }}, - {{ "id": "q-entertain-slider-title", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "Outdoor entertaining size (number of people)" }} }} }} }}, - {{ "id": "q-entertain-slider", "component": {{ "Slider": {{ "value": {{ "path": "guestCount" }}, "minValue": 2, "maxValue": 12 }} }} }}, + {{ "id": "q-entertain-slider-title", "component": "Text", "variant": "h5", "text": "Outdoor entertaining size (number of people)" }}, + {{ "id": "q-entertain-slider", "component": "Slider", "value": {{ "path": "guestCount" }}, "min": 2, "max": 12 }}, - {{ "id": "q-preserve-bushes-check", "component": {{ "CheckBox": {{ "label": {{ "literalString": "Preserve established bushes/trees?" }}, "value": {{ "path": "preserveBushes" }} }} }} }}, + {{ "id": "q-preserve-bushes-check", "component": "CheckBox", "label": "Preserve established bushes/trees?", "value": {{ "path": "preserveBushes" }} }}, - {{ "id": "q-patio-title", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "That concrete patio... what's the plan?" }} }} }} }}, - {{ "id": "q-patio-options", "component": {{ "MultipleChoice": {{ - "selections": {{ "path": "patioPlan" }}, - "maxAllowedSelections": 1, + {{ "id": "q-patio-title", "component": "Text", "variant": "h5", "text": "That concrete patio... what's the plan?" }}, + {{ "id": "q-patio-options", "component": "ChoicePicker", + "value": {{ "path": "patioPlan" }}, + "variant": "multipleSelection", + "label": "Patio Options", "options": [ - {{ "label": {{ "literalString": "Preserve existing paving" }}, "value": "preserve" }}, - {{ "label": {{ "literalString": "Replace with lawn" }}, "value": "lawn" }}, - {{ "label": {{ "literalString": "Replace with decking + lawn" }}, "value": "decking" }} + {{ "label": "Preserve existing paving", "value": "preserve" }}, + {{ "label": "Replace with lawn", "value": "lawn" }}, + {{ "label": "Replace with decking + lawn", "value": "decking" }} ] - }} }} }}, + }}, - {{ "id": "q-submit-button", "component": {{ "Button": {{ "child": "q-submit-button-text", "primary": true, "action": {{ "name": "submit_questionnaire", "context": [ - {{ "key": "preserveBushes", "value": {{ "path": "preserveBushes" }} }}, - {{ "key": "guestCount", "value": {{ "path": "guestCount" }} }}, - {{ "key": "patioPlan", "value": {{ "path": "patioPlan" }} }} - ] }} }} }} }}, - {{ "id": "q-submit-button-text", "component": {{ "Text": {{ "text": {{ "literalString": "Next Page" }} }} }} }} + {{ "id": "q-submit-button", "component": "Button", "child": "q-submit-button-text", "variant": "primary", "action": {{ "event": {{ "name": "submit_questionnaire", "context": {{ + "preserveBushes": {{ "path": "preserveBushes" }}, + "guestCount": {{ "path": "guestCount" }}, + "patioPlan": {{ "path": "patioPlan" }} + }} }} }} }}, + {{ "id": "q-submit-button-text", "component": "Text", "text": "Next Page" }} ] }} }}, - {{ "dataModelUpdate": {{ + {{ "version": "v0.9", "updateDataModel": {{ "surfaceId": "questionnaire", "path": "/", - "contents": [ - {{ "key": "imageUrl", "valueString": "" }}, - {{ "key": "preserveBushes", "valueBoolean": true }}, - {{ "key": "guestCount", "valueNumber": 4 }}, - {{ "key": "patioPlan", "valueArray": ["preserve"] }} - ] + "value": {{ + "imageUrl": "", + "preserveBushes": true, + "guestCount": 4, + "lawnPlan": ["preserve"] + }} }} }} ] ---END QUESTIONNAIRE_EXAMPLE--- ---BEGIN OPTIONS_PRESENTATION_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "options", "root": "options-column", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "options", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "options", "components": [ - {{ "id": "options-column", "component": {{ "Column": {{ "children": {{ "explicitList": ["options-row"] }} }} }} }}, + {{ "id": "root", "component": "Column", "children": ["options-row"] }}, - {{ "id": "options-row", "component": {{ "Column": {{ "children": {{ "explicitList": ["option-card-1", "option-card-2"] }} }} }} }}, + {{ "id": "options-row", "component": "Column", "children": ["option-card-1", "option-card-2"] }}, - {{ "id": "option-card-1", "weight": 1, "component": {{ "Card": {{ "child": "option-layout-1" }} }} }}, - {{ "id": "option-layout-1", "component": {{ "Column": {{ "alignment": "center", "distribution": "center", "children": {{ "explicitList": ["option-image-1", "option-details-1"] }} }} }} }}, - {{ "id": "option-image-1", "component": {{ "Image": {{ "url": {{ "path": "/items/option1/imageUrl" }}, "fit": "cover" }} }} }}, - {{ "id": "option-details-1", "component": {{ "Column": {{ "alignment": "stretch","distribution": "center", "children": {{ "explicitList": ["option-name-1", "option-price-1", "option-time-1", "option-detail-1", "option-tradeoffs-1", "select-button-1"] }} }} }} }}, - {{ "id": "option-name-1", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "path": "/items/option1/name" }} }} }} }}, - {{ "id": "option-price-1", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "/items/option1/price" }} }} }} }}, - {{ "id": "option-time-1", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "/items/option1/time" }} }} }} }}, - {{ "id": "option-detail-1", "component": {{ "Text": {{ "text": {{ "path": "/items/option1/detail" }} }} }} }}, - {{ "id": "option-tradeoffs-1", "component": {{ "Text": {{ "text": {{ "path": "/items/option1/tradeoffs" }} }} }} }}, - {{ "id": "select-button-1", "component": {{ "Button": {{ "primary": true, "child": "select-text-1", "action": {{ "name": "select_option", "context": [ {{ "key": "optionName", "value": {{ "path": "/items/option1/name" }} }}, {{ "key": "optionPrice", "value": {{ "path": "/items/option1/price" }} }} ] }} }} }} }}, - {{ "id": "select-text-1", "component": {{ "Text": {{ "text": {{ "literalString": "Select This Option" }} }} }} }}, + {{ "id": "option-card-1", "component": "Card", "child": "option-layout-1" }}, + {{ "id": "option-layout-1", "component": "Column", "align": "center", "justify": "center", "children": ["option-image-1", "option-details-1"] }}, + {{ "id": "option-image-1", "component": "Image", "url": {{ "path": "/options/items/option1/imageUrl" }}, "fit": "cover" }}, + {{ "id": "option-details-1", "component": "Column", "align": "stretch","justify": "center", "children": ["option-name-1", "option-price-1", "option-time-1", "option-detail-1", "option-tradeoffs-1", "select-button-1"] }}, + {{ "id": "option-name-1", "component": "Text", "variant": "h4", "text": {{ "path": "/options/items/option1/name" }} }}, + {{ "id": "option-price-1", "component": "Text", "variant": "h5", "text": {{ "path": "/options/items/option1/price" }} }}, + {{ "id": "option-time-1", "component": "Text", "variant": "h5", "text": {{ "path": "/options/items/option1/time" }} }}, + {{ "id": "option-detail-1", "component": "Text", "text": {{ "path": "/options/items/option1/detail" }} }}, + {{ "id": "option-tradeoffs-1", "component": "Text", "text": {{ "path": "/options/items/option1/tradeoffs" }} }}, + {{ "id": "select-button-1", "component": "Button", "variant": "primary", "child": "select-text-1", "action": {{ "event": {{ "name": "select_option", "context": {{ "optionName": {{ "path": "/options/items/option1/name" }}, "optionPrice": {{ "path": "/options/items/option1/price" }} }} }} }} }}, + {{ "id": "select-text-1", "component": "Text", "text": "Select This Option" }}, - {{ "id": "option-card-2", "weight": 1, "component": {{ "Card": {{ "child": "option-layout-2" }} }} }}, - {{ "id": "option-layout-2", "component": {{ "Column": {{ "alignment": "center", "distribution": "center", "children": {{ "explicitList": ["option-image-2", "option-details-2"] }} }} }} }}, - {{ "id": "option-image-2", "component": {{ "Image": {{ "url": {{ "path": "/items/option2/imageUrl" }}, "fit": "cover" }} }} }}, - {{ "id": "option-details-2", "component": {{ "Column": {{ "alignment": "stretch","distribution": "center", "children": {{ "explicitList": ["option-name-2", "option-price-2", "option-time-2", "option-detail-2", "option-tradeoffs-2", "select-button-2"] }} }} }} }}, - {{ "id": "option-name-2", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "path": "/items/option2/name" }} }} }} }}, - {{ "id": "option-price-2", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "/items/option2/price" }} }} }} }}, - {{ "id": "option-time-2", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "/items/option2/time" }} }} }} }}, - {{ "id": "option-detail-2", "component": {{ "Text": {{ "text": {{ "path": "/items/option2/detail" }} }} }} }}, - {{ "id": "option-tradeoffs-2", "component": {{ "Text": {{ "text": {{ "path": "/items/option2/tradeoffs" }} }} }} }}, - {{ "id": "select-button-2", "component": {{ "Button": {{ "primary": true, "child": "select-text-2", "action": {{ "name": "select_option", "context": [ {{ "key": "optionName", "value": {{ "path": "/items/option2/name" }} }}, {{ "key": "optionPrice", "value": {{ "path": "/items/option2/price" }} }} ] }} }} }} }}, - {{ "id": "select-text-2", "component": {{ "Text": {{ "text": {{ "literalString": "Select This Option" }} }} }} }} + {{ "id": "option-card-2", "component": "Card", "child": "option-layout-2" }}, + {{ "id": "option-layout-2", "component": "Column", "align": "center", "justify": "center", "children": ["option-image-2", "option-details-2"] }}, + {{ "id": "option-image-2", "component": "Image", "url": {{ "path": "/options/items/option2/imageUrl" }}, "fit": "cover" }}, + {{ "id": "option-details-2", "component": "Column", "align": "stretch","justify": "center", "children": ["option-name-2", "option-price-2", "option-time-2", "option-detail-2", "option-tradeoffs-2", "select-button-2"] }}, + {{ "id": "option-name-2", "component": "Text", "variant": "h4", "text": {{ "path": "/options/items/option2/name" }} }}, + {{ "id": "option-price-2", "component": "Text", "variant": "h5", "text": {{ "path": "/options/items/option2/price" }} }}, + {{ "id": "option-time-2", "component": "Text", "variant": "h5", "text": {{ "path": "/options/items/option2/time" }} }}, + {{ "id": "option-detail-2", "component": "Text", "text": {{ "path": "/options/items/option2/detail" }} }}, + {{ "id": "option-tradeoffs-2", "component": "Text", "text": {{ "path": "/options/items/option2/tradeoffs" }} }}, + {{ "id": "select-button-2", "component": "Button", "variant": "primary", "child": "select-text-2", "action": {{ "event": {{ "name": "select_option", "context": {{ "optionName": {{ "path": "/options/items/option2/name" }}, "optionPrice": {{ "path": "/options/items/option2/price" }} }} }} }} }}, + {{ "id": "select-text-2", "component": "Text", "text": "Select This Option" }} ] }} }}, - {{ "dataModelUpdate": {{ + {{ "version": "v0.9", "updateDataModel": {{ "surfaceId": "options", - "path": "/", - "contents": [ - {{ "key": "items", "valueMap": [ - {{ "key": "option1", "valueMap": [ - {{ "key": "name", "valueString": "Modern Zen Garden" }}, - {{ "key": "detail", "valueString": "Low maintenance, drought-tolerant plants..." }}, - {{ "key": "imageUrl", "valueString": "{base_url}/images/zen_garden.png" }}, - {{ "key": "price", "valueString": "Est. $5,000 - $8,000" }}, - {{ "key": "time", "valueString": "Est. 2-3 weeks" }}, - {{ "key": "tradeoffs", "valueString": "Higher upfront cost, less floral variety." }} - ] }}, - {{ "key": "option2", "valueMap": [ - {{ "key": "name", "valueString": "English Cottage Garden" }}, - {{ "key": "detail", "valueString": "Vibrant, colorful, and teeming with life..." }}, - {{ "key": "imageUrl", "valueString": "{base_url}/images/cottage_garden.png" }}, - {{ "key": "price", "valueString": "Est. $3,000 - $6,000" }}, - {{ "key": "time", "valueString": "Est. 4-6 weeks" }}, - {{ "key": "tradeoffs", "valueString": "Higher maintenance (watering/weeding), seasonal changes.\\n" }} - ] }} - ] }} - ] + "path": "/options", + "value": {{ + "items": {{ + "option1": {{ + "name": "Modern Zen Garden", + "detail": "Low maintenance, drought-tolerant plants...", + "imageUrl": "{base_url}/images/zen_garden.png", + "price": "Est. $5,000 - $8,000", + "time": "Est. 2-3 weeks", + "tradeoffs": "Higher upfront cost, less floral variety." + }}, + "option2": {{ + "name": "English Cottage Garden", + "detail": "Vibrant, colorful, and teeming with life...", + "imageUrl": "{base_url}/images/cottage_garden.png", + "price": "Est. $3,000 - $6,000", + "time": "Est. 4-6 weeks", + "tradeoffs": "Higher maintenance (watering/weeding), seasonal changes.\\n" + }} + }} + }} }} }} ] ---END OPTIONS_PRESENTATION_EXAMPLE--- ---BEGIN SHOPPING_CART_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "cart", "root": "cart-card", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "cart", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "cart", "components": [ - {{ "id": "cart-card", "weight": 1, "component": {{ "Card": {{ "child": "cart-column" }} }} }}, - {{ "id": "cart-column", "component": {{ "Column": {{ "alignment": "stretch", "children": {{ "explicitList": ["cart-subtitle", "item-list", "total-price", "checkout-button"] }} }} }} }}, - {{ "id": "cart-subtitle", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "path": "optionName" }} }} }} }}, - {{ "id": "item-list", "component": {{ "List": {{ "direction": "vertical", "children": {{ "template": {{ "componentId": "item-template", "dataBinding": "/cartItems" }} }} }} }} }}, - {{ "id": "item-template", "component": {{ "Row": {{ "distribution": "spaceBetween", "children": {{ "explicitList": ["template-item-name", "template-item-price"] }} }} }} }}, - {{ "id": "template-item-name", "component": {{ "Text": {{ "text": {{ "path": "name" }} }} }} }}, - {{ "id": "template-item-price", "component": {{ "Text": {{ "text": {{ "path": "price" }} }} }} }}, - {{ "id": "total-price", "component": {{ "Text": {{ "usageHint": "h4", "text": {{ "path": "totalPrice" }} }} }} }}, - {{ "id": "checkout-button", "component": {{ "Button": {{ "child": "checkout-text", "primary": true, "action": {{ "name": "checkout", "context": [ {{ "key": "optionName", "value": {{ "path": "optionName" }} }}, {{ "key": "totalPrice", "value": {{ "path": "totalPrice" }} }} ] }} }} }} }}, - {{ "id": "checkout-text", "component": {{ "Text": {{ "text": {{ "literalString": "Purchase" }} }} }} }} + {{ "id": "root", "weight": 1, "component": "Card", "child": "cart-column" }}, + {{ "id": "cart-column", "component": "Column", "align": "stretch", "children": ["cart-subtitle", "item-list", "total-price", "checkout-button"] }}, + {{ "id": "cart-subtitle", "component": "Text", "variant": "h4", "text": {{ "path": "/cart/optionName" }} }}, + {{ "id": "item-list", "component": "List", "direction": "vertical", "children": {{ "componentId": "item-template", "path": "/cart/cartItems" }} }}, + {{ "id": "item-template", "component": "Row", "justify": "spaceBetween", "children": ["template-item-name", "template-item-price"] }}, + {{ "id": "template-item-name", "component": "Text", "text": {{ "path": "name" }} }}, + {{ "id": "template-item-price", "component": "Text", "text": {{ "path": "price" }} }}, + {{ "id": "total-price", "component": "Text", "variant": "h4", "text": {{ "path": "/cart/totalPrice" }} }}, + {{ "id": "checkout-button", "component": "Button", "child": "checkout-text", "variant": "primary", "action": {{ "event": {{ "name": "checkout", "context": {{ "optionName": {{ "path": "/cart/optionName" }}, "totalPrice": {{ "path": "/cart/totalPrice" }} }} }} }} }}, + {{ "id": "checkout-text", "component": "Text", "text": "Purchase" }} ] }} }}, - {{ "dataModelUpdate": {{ + {{ "version": "v0.9", "updateDataModel": {{ "surfaceId": "cart", - "path": "/", - "contents": [ - {{ "key": "optionName", "valueString": "Modern Zen Garden" }}, - {{ "key": "totalPrice", "valueString": "Total: $7,500.00" }}, - {{ "key": "cartItems", "valueMap": [ - {{ "key": "item1", "valueMap": [ {{ "key": "name", "valueString": "Zen Design Service" }}, {{ "key": "price", "valueString": "$2,000" }} ] }}, - {{ "key": "item2", "valueMap": [ {{ "key": "name", "valueString": "River Rocks (5 tons)" }}, {{ "key": "price", "valueString": "$1,500" }} ] }}, - {{ "key": "item3", "valueMap": [ {{ "key": "name", "valueString": "Japanese Maple Tree" }}, {{ "key": "price", "valueString": "$500" }} ] }}, - {{ "key": "item4", "valueMap": [ {{ "key": "name", "valueString": "Drought-Tolerant Shrubs" }}, {{ "key": "price", "valueString": "$1,000" }} ] }}, - {{ "key": "item5", "valueMap": [ {{ "key": "name", "valueString": "Labor & Installation" }}, {{ "key":"price", "valueString": "$2,500" }} ] }} - ] }} - ] + "path": "/cart", + "value": {{ + "optionName": "Modern Zen Garden", + "totalPrice": "Total: $7,500.00", + "cartItems": {{ + "item1": {{ "name": "Zen Design Service", "price": "$2,000" }}, + "item2": {{ "name": "River Rocks (5 tons)", "price": "$1,500" }}, + "item3": {{ "name": "Japanese Maple Tree", "price": "$500" }}, + "item4": {{ "name": "Drought-Tolerant Shrubs", "price": "$1,000" }}, + "item5": {{ "name": "Labor & Installation", "price": "$2,500" }} + }} + }} }} }} ] ---END SHOPPING_CART_EXAMPLE--- ---BEGIN ORDER_CONFIRMATION_EXAMPLE--- [ - {{ "beginRendering": {{ "surfaceId": "confirmation", "root": "confirmation-card", "styles": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, - {{ "surfaceUpdate": {{ + {{ "version": "v0.9", "createSurface": {{ "surfaceId": "confirmation", "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", "theme": {{ "primaryColor": "#228B22", "font": "Roboto" }} }} }}, + {{ "version": "v0.9", "updateComponents": {{ "surfaceId": "confirmation", "components": [ - {{ "id": "confirmation-card", "weight": 1, "component": {{ "Card": {{ "child": "confirmation-column" }} }} }}, - {{ "id": "confirmation-column", "component": {{ "Column": {{ "alignment": "stretch", "children": {{ "explicitList": ["confirm-icon", "details-column", "confirm-next-steps"] }} }} }} }}, - {{ "id": "confirm-icon", "component": {{ "Icon": {{ "name": {{ "literalString": "check" }} }} }} }}, - {{ "id": "details-column", "component": {{ "Column": {{ "alignment": "stretch", "children": {{ "explicitList": ["design-name-row", "price-row", "order-number-row"] }} }} }} }}, - {{ "id": "design-name-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["design-name-label", "design-name-value"] }} }} }} }}, - {{ "id": "design-name-label", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "Design: " }} }} }} }}, - {{ "id": "design-name-value", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "designName" }} }} }} }}, - {{ "id": "price-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["price-label", "price-value"] }} }} }} }}, - {{ "id": "price-label", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "Price: " }} }} }} }}, - {{ "id": "price-value", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "price" }} }} }} }}, - {{ "id": "order-number-row", "component": {{ "Row": {{ "children": {{ "explicitList": ["order-number-label", "order-number-value"] }} }} }} }}, - {{ "id": "order-number-label", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "literalString": "Order #: " }} }} }} }}, - {{ "id": "order-number-value", "component": {{ "Text": {{ "usageHint": "h5", "text": {{ "path": "orderNumber" }} }} }} }}, - {{ "id": "confirm-next-steps", "component": {{ "Text": {{ "text": {{ "literalString": "Our design team will contact you within 48 hours to schedule an on-site consultation." }} }} }} }} + {{ "id": "root", "weight": 1, "component": "Card", "child": "confirmation-column" }}, + {{ "id": "confirmation-column", "component": "Column", "align": "stretch", "children": ["confirm-icon", "details-column", "confirm-next-steps"] }}, + {{ "id": "confirm-icon", "component": "Icon", "name": "check" }}, + {{ "id": "details-column", "component": "Column", "align": "stretch", "children": ["design-name-row", "price-row", "order-number-row"] }}, + {{ "id": "design-name-row", "component": "Row", "children": ["design-name-label", "design-name-value"] }}, + {{ "id": "design-name-label", "component": "Text", "variant": "h5", "text": "Design: " }}, + {{ "id": "design-name-value", "component": "Text", "variant": "h5", "text": {{ "path": "/confirmation/designName" }} }}, + {{ "id": "price-row", "component": "Row", "children": ["price-label", "price-value"] }}, + {{ "id": "price-label", "component": "Text", "variant": "h5", "text": "Price: " }}, + {{ "id": "price-value", "component": "Text", "variant": "h5", "text": {{ "path": "/confirmation/price" }} }}, + {{ "id": "order-number-row", "component": "Row", "children": ["order-number-label", "order-number-value"] }}, + {{ "id": "order-number-label", "component": "Text", "variant": "h5", "text": "Order #: " }}, + {{ "id": "order-number-value", "component": "Text", "variant": "h5", "text": {{ "path": "/confirmation/orderNumber" }} }}, + {{ "id": "confirm-next-steps", "component": "Text", "text": "Our design team will contact you within 48 hours to schedule an on-site consultation." }} ] }} }}, - {{ "dataModelUpdate": {{ + {{ "version": "v0.9", "updateDataModel": {{ "surfaceId": "confirmation", - "path": "/", - "contents": [ - {{ "key": "designName", "valueString": "Modern Zen Garden" }}, - {{ "key": "price", "valueString": "$7,500.00" }}, - {{ "key": "orderNumber", "valueString": "#LSC-12345" }} - ] + "path": "/confirmation", + "value": {{ + "designName": "Modern Zen Garden", + "price": "$7,500.00", + "orderNumber": "#LSC-12345" + }} }} }} ] ---END ORDER_CONFIRMATION_EXAMPLE--- """ + diff --git a/packages/genai_primitives/example/main.dart b/packages/genai_primitives/example/main.dart index e1770535a..0c5d81acf 100644 --- a/packages/genai_primitives/example/main.dart +++ b/packages/genai_primitives/example/main.dart @@ -9,17 +9,22 @@ import 'dart:typed_data'; import 'package:genai_primitives/genai_primitives.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; void main({void Function(Object?)? output}) { - void print(Object? object) { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { if (output != null) { - output(object); + output(record.message); } else { - core.print(object); + // ignore: avoid_print + core.print(record.message); } - } + }); + + final log = Logger('GenAIPrimitivesExample'); - print('--- GenAI Primitives Example ---'); + log.info('--- GenAI Primitives Example ---'); // 1. Define a Tool final ToolDefinition getWeatherTool = ToolDefinition( @@ -39,8 +44,8 @@ void main({void Function(Object?)? output}) { ), ); - print('\n[Tool Definition]'); - print(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); + log.info('\n[Tool Definition]'); + log.info(const JsonEncoder.withIndent(' ').convert(getWeatherTool.toJson())); // 2. Create a conversation history final history = [ @@ -54,9 +59,9 @@ void main({void Function(Object?)? output}) { ChatMessage.user('What is the weather in London?'), ]; - print('\n[Initial Conversation]'); + log.info('\n[Initial Conversation]'); for (final msg in history) { - print('${msg.role.name}: ${msg.text}'); + log.info('${msg.role.name}: ${msg.text}'); } // 3. Simulate Model Response with Tool Call @@ -73,10 +78,10 @@ void main({void Function(Object?)? output}) { ); history.add(modelResponse); - print('\n[Model Response with Tool Call]'); + log.info('\n[Model Response with Tool Call]'); if (modelResponse.hasToolCalls) { for (final ToolPart call in modelResponse.toolCalls) { - print('Tool Call: ${call.toolName}(${call.arguments})'); + log.info('Tool Call: ${call.toolName}(${call.arguments})'); } } @@ -93,8 +98,8 @@ void main({void Function(Object?)? output}) { ); history.add(toolResult); - print('\n[Tool Result]'); - print('Result: ${toolResult.toolResults.first.result}'); + log.info('\n[Tool Result]'); + log.info('Result: ${toolResult.toolResults.first.result}'); // 5. Simulate Final Model Response with Data (e.g. an image generated or // returned) @@ -110,19 +115,19 @@ void main({void Function(Object?)? output}) { ); history.add(finalResponse); - print('\n[Final Model Response with Data]'); - print('Text: ${finalResponse.text}'); + log.info('\n[Final Model Response with Data]'); + log.info('Text: ${finalResponse.text}'); if (finalResponse.parts.any((p) => p is DataPart)) { final DataPart dataPart = finalResponse.parts.whereType().first; - print( + log.info( 'Attachment: ${dataPart.name} ' '(${dataPart.mimeType}, ${dataPart.bytes.length} bytes)', ); } // 6. Demonstrate JSON serialization of the whole history - print('\n[Full History JSON]'); - print( + log.info('\n[Full History JSON]'); + log.info( const JsonEncoder.withIndent( ' ', ).convert(history.map((m) => m.toJson()).toList()), diff --git a/packages/genai_primitives/pubspec.yaml b/packages/genai_primitives/pubspec.yaml index 9a76d8fd0..10a80a14c 100644 --- a/packages/genai_primitives/pubspec.yaml +++ b/packages/genai_primitives/pubspec.yaml @@ -9,9 +9,7 @@ homepage: https://github.com/flutter/genui/tree/main/packages/genai_primitives license: BSD-3-Clause issue_tracker: https://github.com/flutter/genui/issues -# This package is not included in monorepo, because: -# 1. This dependency path requires one of the genui packages to be excluded from monorepo: genui_dartantic --> dartantic_ai --> genai_primitives -# 2. We do not want the community to get new version of primitives with every change of genui +# This package is not included in monorepo to avoid circular dependencies and unrelated updates. environment: sdk: ">=3.9.2 <4.0.0" @@ -20,6 +18,7 @@ dependencies: collection: ^1.19.1 cross_file: ^0.3.5+1 json_schema_builder: ^0.1.3 + logging: ^1.3.0 meta: ^1.17.0 mime: ^2.0.0 path: ^1.9.1 diff --git a/packages/genui/.guides/docs/connect_to_agent_provider.md b/packages/genui/.guides/docs/connect_to_agent_provider.md deleted file mode 100644 index 9de5b5fbc..000000000 --- a/packages/genui/.guides/docs/connect_to_agent_provider.md +++ /dev/null @@ -1,176 +0,0 @@ ---- -title: Connecting to an agent provider -description: | - Instructions for connecting `genui` to the agent provider of your - choice. See `setup.md` for a description of the different `ContentGenerator` - implementations that are available. ---- - -Follow these steps to connect `genui` to an agent provider and give -your app the ability to send messages and receive/display generated UI. - -The instructions below use a placeholder `YourContentGenerator`. You should -substitute this with your actual `ContentGenerator` implementation (e.g., -`FirebaseAiContentGenerator` from the `genui_firebase_ai` package). - -## 1. Create the `GenUiConversation` - -To connect your app, you'll need to instantiate a `GenUiConversation`. -This class orchestrates the interaction between your UI, the `A2uiMessageProcessor`, -and a `ContentGenerator`. - -1. Create a `A2uiMessageProcessor`, and provide it with the catalog of widgets you want - to make available to the agent. -2. Create a `ContentGenerator` implementation. This is your bridge to the AI - model. You might need to provide system instructions or other - configurations here. -3. Create a `GenUiConversation`, passing in the `A2uiMessageProcessor` and - `ContentGenerator` instances. You can also provide callbacks for UI - events like `onSurfaceAdded`, `onSurfaceUpdated`, `onSurfaceDeleted`, `onTextResponse`, etc. - - For example: - - ```dart - import 'package:flutter/material.dart'; - import 'package:genui/genui.dart'; - import 'package:genui_firebase_ai/genui_firebase_ai.dart'; - - class _MyHomePageState extends State { - late final A2uiMessageProcessor _a2uiMessageProcessor; - late final GenUiConversation _genUiConversation; - final _messages = []; - - @override - void initState() { - super.initState(); - - _a2uiMessageProcessor = A2uiMessageProcessor(catalog: CoreCatalogItems.asCatalog()); - - // Use a concrete implementation of ContentGenerator. - final contentGenerator = FirebaseAiContentGenerator( - catalog: _a2uiMessageProcessor.catalog, - systemInstruction: 'You are a helpful assistant.', - additionalTools: const [], - ); - - _genUiConversation = GenUiConversation( - a2uiMessageProcessor: _a2uiMessageProcessor, - contentGenerator: contentGenerator, - onSurfaceAdded: _onSurfaceAdded, - onSurfaceUpdated: _onSurfaceUpdated, - onSurfaceDeleted: _onSurfaceDeleted, - onTextResponse: (text) => print('AI Text: $text'), - onError: (error) => print('AI Error: ${error.error}'), - ); - } - - void _onSurfaceAdded(SurfaceAdded update) { - setState(() { - _messages.add( - AiUiMessage( - definition: update.definition, - surfaceId: update.surfaceId, - ), - ); - }); - } - - void _onSurfaceUpdated(SurfaceUpdated update) { - // Handle surface updates if needed. For example, you might want to - // scroll to the bottom of a list when a surface is updated. - print('Surface ${update.surfaceId} updated'); - } - - void _onSurfaceDeleted(SurfaceRemoved update) { - setState( - () => _messages.removeWhere( - (m) => m is AiUiMessage && m.surfaceId == update.surfaceId, - ), - ); - } - - @override - void dispose() { - _genUiConversation.dispose(); - // _a2uiMessageProcessor is disposed by _genUiConversation - super.dispose(); - } - } - ``` - -### 2. Send messages and display the agent's responses - -Send a message to the agent using the `sendRequest` method in the `GenUiConversation` -class. - -To receive and display generated UI: - -1. Use `GenUiConversation`'s callbacks (e.g., `onSurfaceAdded`, `onSurfaceDeleted`) - to track the addition and removal of UI surfaces. -2. Build a `GenUiSurface` widget for each active surface ID. - Make sure to provide the host: `_genUiConversation.host`. - - For example: - - ```dart - class _MyHomePageState extends State { - - // ... - - final _textController = TextEditingController(); - final _messages = []; - - // Send a message containing the user's text to the agent. - void _sendMessage(String text) { - if (text.trim().isEmpty) return; - final message = UserMessage.text(text); - setState(() => _messages.add(message)); - _genUiConversation.sendRequest(message); - } - - void _onSurfaceAdded(SurfaceAdded update) { - setState(() { - _messages.add( - AiUiMessage( - definition: update.definition, - surfaceId: update.surfaceId, - ), - ); - }); - } - - // ... other callbacks - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: _messages.length, - itemBuilder: (context, index) { - final message = _messages[index]; - return switch (message) { - AiUiMessage() => GenUiSurface( - host: _genUiConversation.host, - surfaceId: message.surfaceId, - ), - AiTextMessage() => ListTile(title: Text(message.text)), - UserMessage() => ListTile(title: Text(message.text)), - _ => const SizedBox.shrink(), - }; - }, - ), - ), - // ... text input row - ], - ), - ); - } - } - ``` diff --git a/packages/genui/.guides/docs/create_a_custom_catalogitem.md b/packages/genui/.guides/docs/create_a_custom_catalogitem.md deleted file mode 100644 index aa56e4443..000000000 --- a/packages/genui/.guides/docs/create_a_custom_catalogitem.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Create a custom widget and add it to the agent's catalog -description: | - Instructions for creating a custom widget and adding it to the agent's - catalog. ---- - -Follow these steps to create your own, custom widgets and make them available -to the agent for generation. - -## 1. Depend on `json_schema_builder` - -Use `flutter pub add` to add `json_schema_builder` as a dependency in -your `pubspec.yaml` file: - -```bash -flutter pub add json_schema_builder -``` - -## 2. Create the new widget's schema - -Each catalog item needs a schema that defines the data required to populate it. -Using the `json_schema_builder` package, define one for the new widget. - -```dart -import 'package:json_schema_builder/json_schema_builder.dart'; -import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; - -final _schema = S.object( - properties: { - 'question': A2uiSchemas.stringReference(description: 'The question part of a riddle.'), - 'answer': A2uiSchemas.stringReference(description: 'The answer part of a riddle.'), - }, - required: ['question', 'answer'], -); -``` - -## 3. Create a `CatalogItem` - -Each `CatalogItem` represents a type of widget that the agent is allowed to -generate. To do that, combines a name, a schema, and a builder function that -produces the widgets that compose the generated UI. - -The following example creates a `CatalogItem` that displays the question and -answer for a riddle. - -```dart -final riddleCard = CatalogItem( - name: 'RiddleCard', - dataSchema: _schema, - widgetBuilder: ({ - required data, - required id, - required buildChild, - required dispatchEvent, - required context, - required dataContext, - }) { - final json = data as Map; - - final questionNotifier = - dataContext.subscribeToString(json['question'] as Map?); - final answerNotifier = - dataContext.subscribeToString(json['answer'] as Map?); - - // 3. Use ValueListenableBuilder to build the UI reactively - return ValueListenableBuilder( - valueListenable: questionNotifier, - builder: (context, question, _) { - return ValueListenableBuilder( - valueListenable: answerNotifier, - builder: (context, answer, _) { - return Container( - constraints: const BoxConstraints(maxWidth: 400), - decoration: BoxDecoration(border: Border.all()), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(question ?? '', - style: Theme.of(context).textTheme.headlineMedium), - const SizedBox(height: 8.0), - Text(answer ?? '', - style: Theme.of(context).textTheme.headlineSmall), - ], - ), - ); - }, - ); - }, - ); - }, -); -``` - -## 4. Add the `CatalogItem` to the catalog - -Include your catalog items when instantiating `A2uiMessageProcessor`. - -```dart -final a2uiMessageProcessor = A2uiMessageProcessor( - catalog: CoreCatalogItems.asCatalog().copyWith([riddleCard]), -); -``` - -## 5. Update the system instruction to use the new widget - -In order to make sure the agent knows to use your new widget, use the system -instruction to explicitly tell it how and when to do so. Provide the name from -the CatalogItem when you do. - -The following example shows how to instruct an agent provided by Firebase AI -Login to generate a RiddleCard in response to user messages. - -```dart -// In your ContentGenerator implementation (e.g., YourContentGenerator): -final contentGenerator = YourContentGenerator( - systemInstruction: ''' - You are an expert in creating funny riddles. Every time I give you a word, - you should generate a RiddleCard that displays one new riddle related to that word. - Each riddle should have both a question and an answer. - ''', - // Pass any necessary tools to your ContentGenerator -); -``` - -## 6. Using the Data Model - -Your custom widget can also participate in the reactive data model. This allows the AI to create UIs where the state is centralized and can be updated dynamically. - -With the schema and widget builder defined as above, the AI can now generate a `RiddleCard` with either literal values: - -```json -{ - "RiddleCard": { - "question": { "literalString": "What has an eye, but cannot see?" }, - "answer": { "literalString": "A needle." } - } -} -``` - -...or with paths that bind to the data model: - -```json -{ - "RiddleCard": { - "question": { "path": "/riddle/currentQuestion" }, - "answer": { "path": "/riddle/currentAnswer" } - } -} -``` - -When a `path` is used, the `ValueListenableBuilder` in the widget will automatically listen for changes to that path in the `DataModel` and rebuild the widget whenever the data changes. diff --git a/packages/genui/.guides/examples/riddles.dart b/packages/genui/.guides/examples/riddles.dart deleted file mode 100644 index 9f65d1c66..000000000 --- a/packages/genui/.guides/examples/riddles.dart +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; -import 'package:logging/logging.dart'; - -import 'firebase_options.dart'; - -final logger = configureGenUiLogging(level: Level.ALL); -void main() async { - WidgetsFlutterBinding.ensureInitialized(); - - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - - logger.onRecord.listen((record) { - debugPrint('${record.loggerName}: ${record.message}'); - }); - - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), - home: const MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({super.key, required this.title}); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - late final GenUiConversation conversation; - final _textController = TextEditingController(); - final List messages = []; - - void _sendMessage(String text) { - if (text.trim().isEmpty) return; - conversation.sendRequest(UserMessage.text(text)); - _textController.clear(); - } - - @override - void initState() { - super.initState(); - final a2uiMessageProcessor = A2uiMessageProcessor( - catalog: CoreCatalogItems.asCatalog().copyWith([riddleCard]), - ); - final contentGenerator = FirebaseAiContentGenerator( - systemInstruction: ''' - You are an expert in creating funny riddles. Every time I give you a - word, you should generate a RiddleCard that displays one new riddle - related to that word. Each riddle should have both a question and an - answer. - ''', - ); - conversation = GenUiConversation( - contentGenerator: contentGenerator, - a2uiMessageProcessor: a2uiMessageProcessor, - onSurfaceAdded: (update) { - setState(() { - messages.add( - AiUiMessage( - definition: update.definition, - surfaceId: update.surfaceId, - ), - ); - }); - }, - onTextResponse: (text) { - setState(() { - messages.add(AiTextMessage.text(text)); - }); - }, - onError: (error) { - setState(() { - messages.add(InternalMessage('Error: ${error.error}')); - }); - }, - ); - conversation.conversation.addListener(() { - // This is just to trigger a rebuild when the conversation history inside - // GenUiConversation changes. - setState(() {}); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: Column( - children: [ - Expanded( - child: ListView.builder( - itemCount: messages.length, - itemBuilder: (context, index) { - final message = messages[index]; - return switch (message) { - AiUiMessage() => GenUiSurface( - key: message.uiKey, - host: conversation.host, - surfaceId: message.surfaceId, - ), - AiTextMessage() => ChatMessageWidget( - text: message.text, - isUser: false, - ), - UserMessage() => ChatMessageWidget( - text: message.text, - isUser: true, - ), - InternalMessage() => InternalMessageWidget( - content: message.text, - ), - _ => Text(message.toString()), - }; - }, - ), - ), - if (conversation.isProcessing.value) const LinearProgressIndicator(), - SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - Expanded( - child: TextField( - controller: _textController, - decoration: const InputDecoration( - hintText: 'Enter a message', - ), - onSubmitted: _sendMessage, - ), - ), - const SizedBox(width: 16), - ElevatedButton( - onPressed: () => _sendMessage(_textController.text), - child: const Text('Send'), - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -class ChatMessageWidget extends StatelessWidget { - const ChatMessageWidget({ - super.key, - required this.text, - required this.isUser, - }); - - final String text; - final bool isUser; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: isUser - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - Icon(isUser ? Icons.person : Icons.computer), - const SizedBox(width: 8), - Flexible( - child: Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: isUser ? Colors.blue[100] : Colors.grey[200], - borderRadius: BorderRadius.circular(8), - ), - child: Text(text), - ), - ), - ], - ); - } -} - -class InternalMessageWidget extends StatelessWidget { - const InternalMessageWidget({super.key, required this.content}); - final String content; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(8), - margin: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.yellow[100], - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon(Icons.warning, color: Colors.orange), - const SizedBox(width: 8), - Expanded(child: Text(content)), - ], - ), - ); - } -} - -final _schema = S.object( - properties: { - 'question': A2uiSchemas.stringReference( - description: 'The question part of a riddle.', - ), - 'answer': A2uiSchemas.stringReference( - description: 'The answer part of a riddle.', - ), - }, - required: ['question', 'answer'], -); - -final riddleCard = CatalogItem( - name: 'RiddleCard', - dataSchema: _schema, - widgetBuilder: (context) { - final questionNotifier = context.dataContext.subscribeToString( - context.data['question'] as Map?, - ); - final answerNotifier = context.dataContext.subscribeToString( - context.data['answer'] as Map?, - ); - - return ValueListenableBuilder( - valueListenable: questionNotifier, - builder: (context, question, _) { - return ValueListenableBuilder( - valueListenable: answerNotifier, - builder: (context, answer, _) { - return Container( - constraints: const BoxConstraints(maxWidth: 400), - decoration: BoxDecoration(border: Border.all()), - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - question ?? '', - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 8.0), - Text( - answer ?? '', - style: Theme.of(context).textTheme.headlineSmall, - ), - ], - ), - ); - }, - ); - }, - ); - }, -); diff --git a/packages/genui/.guides/setup.md b/packages/genui/.guides/setup.md deleted file mode 100644 index 4cc0bced6..000000000 --- a/packages/genui/.guides/setup.md +++ /dev/null @@ -1,115 +0,0 @@ -# Set up of `genui` - -Use the following instructions to add `genui` to your Flutter app. The -code examples show how to perform the instructions on a brand new app created by -running `flutter create`. - -## 1. Choosing a ContentGenerator - -`genui` provides three `ContentGenerator` implementations for connecting to -different types of agent providers. Use the following guide to choose the one -that best fits your needs. - -- **`FirebaseAiContentGenerator`**: Use this when you are ready to deploy your - app. This generator connects to Gemini via Firebase AI Logic, and is the - recommended approach for production apps. - -- **`GoogleGenerativeAiContentGenerator`**: Use this for prototyping and local - development. This generator connects to the Google Generative AI API using an - API key, and is a good choice for getting started quickly. - -- **`A2uiContentGenerator`**: Use this to connect to a server that implements - the A2UI protocol. This is a good choice if you have an existing agent that - you want to connect to `genui`. - -## 2. Configure your agent provider - -`genui` can connect to a variety of agent providers. Choose the section -below for your preferred provider. - -### Configure Firebase AI Logic - -To use the built-in `FirebaseContentGenerator` to connect to Gemini via Firebase AI -Logic, follow these instructions: - -1. [Create a new Firebase project](https://support.google.com/appsheet/answer/10104995) - using the Firebase Console. This should be done by a human. -2. [Enable the Gemini API](https://firebase.google.com/docs/gemini-in-firebase/set-up-gemini) - for that project. This should also be done by a human. -3. Follow the first three steps in - [Firebase's Flutter Setup guide](https://firebase.google.com/docs/flutter/setup) - to add Firebase to your app. Run `flutterfire configure` to configure your - app. -4. Use `flutter pub add` to add the `genui` and `genui_firebase_ai` packages as - dependencies in your `pubspec.yaml` file: - - ```bash - flutter pub add genui genui_firebase_ai - ``` - -5. In your app's `main` method, ensure that the widget bindings are initialized, - and then initialize Firebase. - - ```dart - void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - runApp(const MyApp()); - } - ``` - -### Configure with the A2UI protocol - -To use `genui` with a generic agent provider that supports the A2UI protocol, -use the `genui_a2ui` package. - -1. Use `flutter pub add` to add the `genui` and `genui_a2ui` packages as - dependencies in your `pubspec.yaml` file: - - ```bash - flutter pub add genui genui_a2ui - ``` - -2. Use the `A2uiContentGenerator` to connect to your agent provider. - -### Configure with Google Generative AI - -To use `genui` with the Google Generative AI API, use the -`genui_google_generative_ai` package. - -1. Use `flutter pub add` to add the `genui` and `genui_google_generative_ai` packages as - dependencies in your `pubspec.yaml` file: - - ```bash - flutter pub add genui genui_google_generative_ai - ``` - -2. Use the `GoogleGenerativeAiContentGenerator` to connect to the Google - Generative AI API. You will need to provide your own API key. - -### Connecting to a custom backend - -There are two ways to connect to a custom backend. - -1. Use the `genui_a2ui` package to connect to a server that implements the - A2UI protocol. This is the recommended approach if you have an existing - agent that you want to connect to `genui`. - -2. Implement your own `ContentGenerator`. This is a good choice if you have a - custom backend that does not implement the A2UI protocol. This will require - you to convert your UI data source into a stream of `A2uiMessage` messages - that can be rendered by the Gen UI SDK. - -## 2. Create the connection to an agent - -If you build your Flutter project for iOS or macOS, add this key to your -`{ios,macos}/Runner/*.entitlements` file(s) to enable outbound network -requests: - -```xml - -... -com.apple.security.network.client - - -``` diff --git a/packages/genui/.guides/usage.md b/packages/genui/.guides/usage.md deleted file mode 100644 index d6439858d..000000000 --- a/packages/genui/.guides/usage.md +++ /dev/null @@ -1,47 +0,0 @@ -# `genui` Package Context for AI Agents - -## Purpose - -This document provides context for AI agents working with the `genui` package. This package is the core framework for building generative user interfaces in Flutter, where the UI is dynamically constructed by an AI model in real-time. - -## Key Concepts - -- **`GenUiConversation`**: The main facade and entry point. It manages the conversation loop, orchestrates the `A2uiMessageProcessor` and `ContentGenerator`, and handles the flow of messages. -- **`ContentGenerator`**: An abstract interface for communicating with AI models. Concrete implementations (like `FirebaseAiContentGenerator` in `genui_firebase_ai`) handle specific model APIs. -- **`A2uiMessageProcessor`**: Manages the state of the dynamic UI, including active surfaces and the `DataModel`. -- **`Catalog`**: A registry of `CatalogItem`s (widgets) that the AI can use. Each item has a schema and a builder. -- **`DataModel`**: A centralized, observable store for UI state. Widgets are bound to paths in this model and update reactively. -- **`A2uiMessage`**: The protocol for AI commands to the UI (e.g., `SurfaceUpdate`, `BeginRendering`). - -## Architecture - -See [DESIGN.md](./DESIGN.md) for a detailed architectural overview. - -The package is layered: - -1. **Content Generator Layer**: `lib/src/content_generator.dart` (AI communication). -2. **UI State Management Layer**: `lib/src/core/` (`A2uiMessageProcessor`, `ui_tools.dart`). -3. **UI Model Layer**: `lib/src/model/` (Data structures like `A2uiMessage`, `UiDefinition`, `DataModel`). -4. **Widget Catalog Layer**: `lib/src/catalog/` (Core widgets like `Text`, `Button`, `Column`). -5. **UI Facade Layer**: `lib/src/conversation/` (`GenUiConversation`, `GenUiSurface`). - -## Usage - -See [README.md](./README.md) for getting started guides and examples. - -Typical usage involves: - -1. Initializing a `A2uiMessageProcessor` with a `Catalog`. -2. Initializing a `ContentGenerator` (e.g., `FirebaseAiContentGenerator`). -3. Creating a `GenUiConversation` with the manager and generator. -4. Sending user prompts via `genUiConversation.sendRequest()`. -5. Rendering `GenUiSurface` widgets based on surface IDs from `onSurfaceAdded` callbacks. - -## File Structure - -- `lib/genui.dart`: Main export file. -- `lib/src/catalog/`: Core widget implementations (`core_catalog.dart`, `core_widgets/`). -- `lib/src/content_generator.dart`: `ContentGenerator` interface. -- `lib/src/conversation/`: `GenUiConversation` and `GenUiSurface`. -- `lib/src/core/`: `A2uiMessageProcessor`, `GenUiConfiguration`, `ui_tools.dart`. -- `lib/src/model/`: Data models (`a2ui_message.dart`, `data_model.dart`, `catalog.dart`). diff --git a/packages/genui/CHANGELOG.md b/packages/genui/CHANGELOG.md index 0832b077a..7687740f7 100644 --- a/packages/genui/CHANGELOG.md +++ b/packages/genui/CHANGELOG.md @@ -7,6 +7,17 @@ - **Fix**: Improved error handling for catalog example loading to include context about the invalid item (#653). - **BREAKING**: Renamed `ChatMessageWidget` to `ChatMessageView` and `InternalMessageWidget` to `InternalMessageView` (#661). - **Fix**: Pass the correct `catalogId` in `DebugCatalogView` widget (#676). +- **BREAKING**: Renamed most classes with `GenUi` prefix to remove the prefix or use `Surface`. + - `GenUiConversation` -> `Conversation` + - `GenUiController` -> `SurfaceController` + - `GenUiSurface` -> `Surface` + - `GenUiHost` -> `SurfaceHost` + - `GenUiContext` -> `SurfaceContext` + - `GenUiTransport` -> `Transport` + - `GenUiPromptFragments` -> `PromptFragments` + - `GenUiFunctionDeclaration` -> `ClientFunction` + - `GenUiFallback` -> `FallbackWidget` + - `configureGenUiLogging` -> `configureLogging` - Added some dart documentation and an `example` directory to improve `package:genui` pub score. - **Fix**: Make `ContentGeneratorError` be an `Exception` (#660). - **Feature**: Define genui parts as extensions of `genai_primitives` (#675). @@ -29,7 +40,7 @@ - **Feature**: `GenUiManager` now supports multiple catalogs by accepting an `Iterable` in its constructor. - **Feature**: `A2uiMessageProcessor` now supports multiple catalogs by accepting an `Iterable` in its constructor. - **Feature**: `catalogId` property added to `UiDefinition` to specify which catalog a UI surface should use. -- **Refactor**: Moved `standardCatalogId` constant from `core_catalog.dart` to `primitives/constants.dart` for better organization and accessibility. +- **Refactor**: Moved `standardCatalogId` constant from `basic_catalog.dart` to `primitives/constants.dart` for better organization and accessibility. - **Fix**: `MultipleChoice` widget now correctly handles `maxAllowedSelections` when provided as a `double` in JSON, preventing type cast errors. - **Fix**: The `Text` catalog item now respects the ambient `DefaultTextStyle`, resolving contrast issues where, for example, text inside a dark purple primary `Button` would be black instead of white. @@ -57,9 +68,9 @@ ## 0.2.0 - **BREAKING**: Replaced `ElevatedButton` with a more generic `Button` component. -- **BREAKING**: Removed `CheckboxGroup` and `RadioGroup` from the core catalog. The `MultipleChoice` or `CheckBox` widgets can be used as replacements. +- **BREAKING**: Removed `CheckboxGroup` and `RadioGroup` from the basic catalog. The `MultipleChoice` or `CheckBox` widgets can be used as replacements. - **Feature**: Added an `obscured` property to `TextInputChip` to allow for password style inputs. -- **Feature**: Added many new components to the core catalog: `AudioPlayer` (placeholder), `Button`, `Card`, `CheckBox`, `DateTimeInput`, `Divider`, `Heading`, `List`, `Modal`, `MultipleChoice`, `Row`, `Slider`, `Tabs`, and `Video` (placeholder). +- **Feature**: Added many new components to the basic catalog: `AudioPlayer` (placeholder), `Button`, `Card`, `CheckBox`, `DateTimeInput`, `Divider`, `Heading`, `List`, `Modal`, `MultipleChoice`, `Row`, `Slider`, `Tabs`, and `Video` (placeholder). - **Fix**: Corrected the action key from `actionName` to `name` in `Trailhead` and `TravelCarousel`. - **Fix**: Corrected the image property from `location` to `url` in `TravelCarousel`. diff --git a/packages/genui/DESIGN.md b/packages/genui/DESIGN.md deleted file mode 100644 index fc7fcd3dc..000000000 --- a/packages/genui/DESIGN.md +++ /dev/null @@ -1,178 +0,0 @@ -# `genui` Package Implementation - -This document provides a comprehensive overview of the architecture, purpose, and implementation of the `genui` package. - -## Purpose - -The `genui` package provides the core framework for building Flutter applications with dynamically generated user interfaces powered by large language models (LLMs). It enables developers to create conversational UIs where the interface is not static or predefined, but is instead constructed by an AI in real-time based on the user's prompts and the flow of the conversation. - -The package supplies the essential components for managing the state of the dynamic UI, interacting with the AI model, defining a vocabulary of UI widgets, and rendering the UI surfaces. The primary entry point for this package is the `GenUiConversation`. - -## Architecture - -The package is designed with a layered architecture, separating concerns to create a flexible and extensible framework. The diagram below shows how the `genui` package integrates with the developer's application and the backend LLM. - -```mermaid -graph TD - subgraph "Developer's Application" - AppLogic["App Logic"] - UIWidgets["UI Widgets
(e.g., GenUiSurface)"] - end - - subgraph "genui Package" - GenUiConversation["GenUiConversation (Facade)"] - ContentGenerator["ContentGenerator
(e.g., a Gemini client)"] - A2uiMessageProcessor["A2uiMessageProcessor
(Manages Surfaces, Tools, State)"] - Catalog["Widget Catalog"] - DataModel["DataModel"] - end - - subgraph "Backend" - LLM["Large Language Model"] - end - - AppLogic -- "Initializes and Uses" --> GenUiConversation - GenUiConversation -- "Encapsulates" --> A2uiMessageProcessor - GenUiConversation -- "Encapsulates" --> ContentGenerator - - A2uiMessageProcessor -- "Owns" --> DataModel - - AppLogic -- "Sends User Input" --> GenUiConversation - GenUiConversation -- "Manages Conversation &
Calls sendRequest()" --> ContentGenerator - ContentGenerator -- "Sends prompt +
tool schemas" --> LLM - LLM -- "Returns tool call" --> ContentGenerator - - ContentGenerator -- "Executes tool" --> A2uiMessageProcessor - - A2uiMessageProcessor -- "Notifies of updates" --> UIWidgets - UIWidgets -- "Builds widgets using" --> Catalog - UIWidgets -- "Reads/writes state via" --> DataModel - UIWidgets -- "Sends UI events to" --> A2uiMessageProcessor - - A2uiMessageProcessor -- "Puts user input on stream" --> GenUiConversation -``` - -### 1. Content Generator Layer (`lib/src/content_generator.dart`) - -This layer is responsible for all communication with the generative AI model. - -- **`ContentGenerator`**: An abstract interface defining the contract for a client that interacts with an AI model. This allows for different LLM backends to be implemented. It exposes the following streams: - - `a2uiMessageStream`: Emits `A2uiMessage` objects representing AI commands to modify text responses from the AI. - - `errorStream`: Emits `ContentGeneratorError` objects when issues occur during AI interaction. -- **Example Implementations**: The `genui_firebase_ai` package provides a concrete implementation that uses Google's Gemini models via Firebase. It handles the complexities of interacting with the Gemini API, including model configuration, retry logic, and tool management. -- **`AiTool`**: An abstract class for defining tools that the AI can invoke. These tools are the bridge between the AI and the application's capabilities. The `DynamicAiTool` provides a convenient way to create tools from simple functions. - -### 2. UI State Management Layer (`lib/src/core/`) - -This is the central nervous system of the package, orchestrating the state of all generated UI surfaces. - -- **`A2uiMessageProcessor`**: The core state manager for the dynamic UI. It maintains a map of all active UI "surfaces", where each surface is represented by a `UiDefinition`. It takes a `GenUiConfiguration` object that can restrict AI actions (e.g., only allow creating surfaces, not updating or deleting them). The AI interacts with the manager by invoking tools defined in `ui_tools.dart` (`SurfaceUpdateTool`, `DeleteSurfaceTool`, `BeginRenderingTool`), which in turn call `a2uiMessageProcessor.handleMessage()`. It exposes a stream of `GenUiUpdate` events (`SurfaceAdded`, `SurfaceUpdated`, `SurfaceRemoved`) so that the application can react to changes. It also owns the `DataModel` to manage the state of individual widgets (e.g., text field content) and acts as the `GenUiHost` for the `GenUiSurface` widget. -- **`ui_tools.dart`**: Contains the `SurfaceUpdateTool` and `DeleteSurfaceTool` classes that wrap the `A2uiMessageProcessor`'s methods, making them available to the AI. - -### 3. UI Model Layer (`lib/src/model/`) - -This layer defines the data structures that represent the dynamic UI and the conversation. - -- **`Catalog` and `CatalogItem`**: These classes define the registry of available UI components. The `Catalog` holds a list of `CatalogItem`s, and each `CatalogItem` defines a widget's name, its data schema, and a builder function to render it. -- **`A2uiMessage`**: A sealed class (`lib/src/model/a2ui_message.dart`) representing the commands the AI sends to the UI. It has the following subtypes: - - `BeginRendering`: Signals the start of rendering for a surface, specifying the root component. - - `SurfaceUpdate`: Adds or updates components on a surface. - - `DataModelUpdate`: Modifies data within the `DataModel` for a surface. - - `SurfaceDeletion`: Requests the removal of a surface. - The schemas for these messages are defined in `lib/src/model/a2ui_schemas.dart`. -- **`UiDefinition` and `UiEvent`**: `UiDefinition` represents a complete UI tree to be rendered, including the root widget and a map of all widget definitions. `UiEvent` is a data object representing a user interaction. `UserActionEvent` is a subtype used for events that should trigger a submission to the AI, like a button tap. -- **`ChatMessage`**: A sealed class representing the different types of messages in a conversation: `UserMessage`, `AiTextMessage`, `ToolResponseMessage`, `AiUiMessage`, `InternalMessage`, and `UserUiInteractionMessage`. -- **`DataModel` and `DataContext`**: The `DataModel` is a centralized, observable key-value store that holds the entire dynamic state of the UI. Widgets receive a `DataContext`, which is a view into the `DataModel` that understands the widget's current scope. This allows widgets to subscribe to changes in the data model and rebuild reactively. This separation of data and UI structure is a core principle of the architecture. - -### 4. Widget Catalog Layer (`lib/src/catalog/`) - -This layer provides a set of core, general-purpose UI widgets that can be used out-of-the-box. - -- **`core_catalog.dart`**: Defines the `CoreCatalogItems`, which includes fundamental widgets like `AudioPlayer`, `Button`, `Card`, `CheckBox`, `Column`, `DateTimeInput`, `Divider`, `Icon`, `Image`, `List`, `Modal`, `MultipleChoice`, `Row`, `Slider`, `Tabs`, `Text`, `TextField`, and `Video`. -- **Widget Implementation**: Each core widget follows the standard `CatalogItem` pattern: a schema definition, a type-safe data accessor using an `extension type`, the `CatalogItem` instance, and the Flutter widget implementation. - -### 5. UI Facade Layer (`lib/src/conversation/`) - -This layer provides high-level widgets and controllers for easily building a generative UI application. - -- **`GenUiConversation`**: The primary entry point for the package. This facade class encapsulates the `A2uiMessageProcessor` and `ContentGenerator`, managing the conversation loop and orchestrating the entire generative UI process. It listens to the streams from the `ContentGenerator` and routes messages accordingly (e.g., `A2uiMessage` to `A2uiMessageProcessor`, text to `onTextResponse` callback). -- **`GenUiSurface`**: The Flutter widget responsible for recursively building a UI tree from a `UiDefinition`. It listens for updates from a `GenUiHost` (typically the `A2uiMessageProcessor`) for a specific `surfaceId` and rebuilds itself when the definition changes. - -### 6. Primitives Layer (`lib/src/primitives/`) - -This layer contains basic utilities used throughout the package. - -- **`logging.dart`**: Provides a configurable logger (`genUiLogger`). -- **`simple_items.dart`**: Defines a type alias for `JsonMap`. - -### 7. Direct Call Integration (`lib/src/facade/direct_call_integration/`) - -This directory provides utilities for a more direct interaction with the AI model, potentially bypassing some of the higher-level abstractions of `GenUiConversation`. It includes: - -- **`model.dart`**: Defines data models for direct API calls. -- **`utils.dart`**: Contains utility functions to assist with direct calls. - -## How It Works: The Generative UI Cycle - -The `GenUiConversation` simplifies the process of creating a generative UI by managing the conversation loop and the interaction with the AI. - -```mermaid -sequenceDiagram - participant User - participant AppLogic as "App Logic (Developer's Code)" - participant GenUiConversation - participant ContentGenerator - participant LLM - participant A2uiMessageProcessor - participant GenUiSurface as "GenUiSurface" - - AppLogic->>+GenUiConversation: Creates GenUiConversation(a2uiMessageProcessor, contentGenerator) - AppLogic->>+GenUiConversation: (Optional) sendRequest(instructionMessage) - - User->>+AppLogic: Provides input (e.g., text prompt) - AppLogic->>+GenUiConversation: Calls sendRequest(userMessage) - GenUiConversation->>GenUiConversation: Manages conversation history - GenUiConversation->>+ContentGenerator: Calls sendRequest(conversation) - ContentGenerator->>+LLM: Sends prompt and tool schemas - LLM-->>-ContentGenerator: Responds with content (A2UI messages, text) - - ContentGenerator-->>GenUiConversation: Emits A2uiMessage on a2uiMessageStream - GenUiConversation->>+A2uiMessageProcessor: Calls a2uiMessageProcessor.handleMessage(a2uiMessage) - A2uiMessageProcessor->>A2uiMessageProcessor: Updates state and broadcasts GenUiUpdate - A2uiMessageProcessor-->>-GenUiConversation: (No return value from handleMessage) - - ContentGenerator-->>GenUiConversation: Emits text on textResponseStream - GenUiConversation->>AppLogic: Calls onTextResponse callback - - ContentGenerator-->>GenUiConversation: Emits error on errorStream - GenUiConversation->>AppLogic: Calls onError callback - - %% The UI updates asynchronously based on the stream - A2uiMessageProcessor->>GenUiSurface: Notifies of the update via a Stream - activate GenUiSurface - GenUiSurface->>GenUiSurface: Rebuilds its widget tree using the new definition - GenUiSurface->>User: Renders the new UI - deactivate GenUiSurface - - %% Later, the user interacts - User->>+GenUiSurface: Interacts with a widget (e.g., clicks a button) - GenUiSurface-->>A2uiMessageProcessor: Fires onEvent, which calls handleUiEvent() - A2uiMessageProcessor-->>GenUiConversation: Emits a UserMessage on the onSubmit stream - GenUiConversation->>GenUiConversation: Receives UserMessage, adds to conversation - deactivate GenUiConversation - Note over GenUiConversation, LLM: The cycle repeats... -``` - -1. **Initialization**: The developer creates a `GenUiConversation`, providing it with a `A2uiMessageProcessor` and a `ContentGenerator`. The developer may also provide a system instruction to the `GenUiConversation` by sending an an initial `UserMessage`. -2. **User Input**: The user enters a prompt. -3. **Send Request**: The developer calls `genUiConversation.sendRequest(UserMessage.text(prompt))`. -4. **Conversation Management**: The `GenUiConversation` adds the `UserMessage` to its internal conversation history. -5. **AI Invocation**: The `GenUiConversation` calls `contentGenerator.sendRequest()`, passing in the conversation history. -6. **Model Processing & Response**: The LLM processes the conversation and the `ContentGenerator` emits responses on its streams. -7. **A2UI Message Handling**: When an `A2uiMessage` is received on the `a2uiMessageStream`, `GenUiConversation` calls `a2uiMessageProcessor.handleMessage()` with the message (e.g., `SurfaceUpdate`, `BeginRendering`). -8. **State Update & Notification**: The `A2uiMessageProcessor` updates its internal state (the `UiDefinition` for the surface) based on the `A2uiMessage` and broadcasts a `GenUiUpdate` event on its `surfaceUpdates` stream. -9. **Text/Error Handling**: Text responses or errors from the `ContentGenerator`'s other streams trigger the `onTextResponse` or `onError` callbacks, respectively. -10. **UI Rendering**: A `GenUiSurface` widget listening to the `A2uiMessageProcessor` (via the `GenUiHost` interface) receives the update and rebuilds, rendering the new UI based on the updated `UiDefinition`. -11. **User Interaction**: The user interacts with the newly generated UI (e.g., clicks a submit button). -12. **Event Dispatch**: The widget's builder calls a `dispatchEvent` function, which causes the `GenUiSurface` to call `host.handleUiEvent()`. -13. **Cycle Repeats**: The `A2uiMessageProcessor`'s `handleUiEvent` method creates a `UserMessage` containing the state of the widgets on the surface (from its `DataModel`) and emits it on its `onSubmit` stream. The `GenUiConversation` is listening to this stream, receives the message, adds it to the conversation, and calls the AI again, thus continuing the cycle. diff --git a/packages/genui/README.md b/packages/genui/README.md index 9cb013f2d..25f32e337 100644 --- a/packages/genui/README.md +++ b/packages/genui/README.md @@ -4,43 +4,45 @@ A Flutter package for building dynamic, conversational user interfaces powered b `genui` allows you to create applications where the UI is not static or predefined, but is instead constructed by an AI in real-time based on a conversation with the user. This enables highly flexible, context-aware, and interactive user experiences. -This package provides the core functionality for GenUI. For concrete implementations, see the `genui_firebase_ai` package (for Firebase AI) or the `genui_a2ui` package (for a generic A2UI server). +This package provides the core functionality for GenUI. ## Features - **Dynamic UI Generation**: Render Flutter UIs from structured data returned by a generative AI. -- **Simplified Conversation Flow**: A high-level `GenUiConversation` facade manages the interaction loop with the AI. +- **Simplified Conversation Flow**: A high-level `Conversation` facade manages the interaction loop with the AI. - **Customizable Widget Catalog**: Define a "vocabulary" of Flutter widgets that the AI can use to build the interface. -- **Extensible Content Generator**: Abstract interface for connecting to different AI model backends. +- **SurfaceController**: High-level controller that manages the input/output pipeline and UI state. +- **Parser Transformer**: `A2uiParserTransformer` for robustly parsing A2UI message streams from text chunks. - **Event Handling**: Capture user interactions (button clicks, text input), update a client-side data model, and send the state back to the AI as context for the next turn in the conversation. - **Reactive UI**: Widgets automatically rebuild when the data they are bound to changes in the data model. ## Core Concepts The package is built around the following main components: - -1. **`GenUiConversation`**: The primary facade and entry point for the package. It encapsulates the `A2uiMessageProcessor` and `ContentGenerator`, manages the conversation history, and orchestrates the entire generative UI process. +1. **`Conversation`**: The primary facade and entry point for the package. It encapsulates the `SurfaceController` and `Transport`, manages the conversation events, and orchestrates the entire generative UI process. 2. **`Catalog`**: A collection of `CatalogItem`s that defines the set of widgets the AI is allowed to use. Each `CatalogItem` specifies a widget's name (for the AI to reference), a data schema for its properties, and a builder function to render the Flutter widget. 3. **`DataModel`**: A centralized, observable store for all dynamic UI state. Widgets are "bound" to data in this model. When data changes, only the widgets that depend on that specific piece of data are rebuilt. -4. **`ContentGenerator`**: An interface for communicating with a generative AI model. This interface uses streams to send `A2uiMessage` commands, text responses, and errors back to the `GenUiConversation`. +4. **`SurfaceController`**: The runtime engine that manages the lifecycle of UI surfaces, handles data model updates, and orchestrates the application of A2UI messages. + +5. **`A2uiTransportAdapter`**: An implementation of `Transport` that wraps `A2uiParserTransformer` to parse raw text chunks (e.g. from an LLM stream) into structured `GenerationEvent`s. -5. **`A2uiMessage`**: A message sent from the AI (via the `ContentGenerator`) to the UI, instructing it to perform actions like `beginRendering`, `surfaceUpdate`, `dataModelUpdate`, or `deleteSurface`. +6. **`A2uiMessage`**: A message sent from the AI to the UI, instructing it to perform actions like `createSurface`, `updateComponents`, `updateDataModel`, or `deleteSurface`. ## How It Works -The `GenUiConversation` manages the interaction cycle: +The `Conversation`, `SurfaceController`, and `A2uiTransportAdapter` manage the interaction cycle: -1. **User Input**: The user provides a prompt (e.g., through a text field). The app calls `genUiConversation.sendRequest()`. -2. **AI Invocation**: The `GenUiConversation` adds the user's message to its internal conversation history and calls `contentGenerator.sendRequest()`. -3. **AI Response**: The `ContentGenerator` interacts with the AI model. The AI, guided by the widget schemas, sends back responses. -4. **Stream Handling**: The `ContentGenerator` emits `A2uiMessage`s, text responses, or errors on its streams. -5. **UI State Update**: `GenUiConversation` listens to these streams. `A2uiMessage`s are passed to `A2uiMessageProcessor.handleMessage()`, which updates the UI state and `DataModel`. -6. **UI Rendering**: The `A2uiMessageProcessor` broadcasts an update, and any `GenUiSurface` widgets listening for that surface ID will rebuild. Widgets are bound to the `DataModel`, so they update automatically when their data changes. -7. **Callbacks**: Text responses and errors trigger the `onTextResponse` and `onError` callbacks on `GenUiConversation`. -8. **User Interaction**: The user interacts with the newly generated UI (e.g., by typing in a text field). This interaction directly updates the `DataModel`. If the interaction is an action (like a button click), the `GenUiSurface` captures the event and forwards it to the `GenUiConversation`'s `A2uiMessageProcessor`, which automatically creates a new `UserMessage` containing the current state of the data model and restarts the cycle. +1. **User Input**: The user provides a prompt. The app calls `conversation.sendRequest()`. +2. **AI Invocation**: The `Conversation` triggers `A2uiTransportAdapter.onSend`. +3. **Stream Handling**: The app's `onSend` implementation calls the LLM and pipes the response chunks to `A2uiTransportAdapter.addChunk()`. +4. **Parsing**: The `A2uiTransportAdapter` uses `A2uiParserTransformer` to parse chunks into `TextEvent`s or `A2uiMessageEvent`s. +5. **UI State Update**: `SurfaceController` handles the messages and updates the `DataModel`. +6. **UI Rendering**: `Surface` widgets listening to `SurfaceController` rebuild automatically. +7. **User Interaction**: User actions (buttons, etc.) trigger events. `SurfaceController` captures them and emits `ChatMessage` events on `onSubmit`. +8. **Loop**: `Conversation` listens to `onSubmit` and automatically triggers a new request to the AI, continuing the conversation. ```mermaid graph TD @@ -50,21 +52,24 @@ graph TD end subgraph "GenUI Framework" - GenUiConversation("GenUiConversation") - ContentGenerator("ContentGenerator") - A2uiMessageProcessor("A2uiMessageProcessor") - GenUiSurface("GenUiSurface") + Conversation("Conversation") + Transport("A2uiTransportAdapter") + SurfaceController("SurfaceController") + Transformer("A2uiParserTransformer") + Surface("Surface") end - UserInput -- "calls sendRequest()" --> GenUiConversation; - GenUiConversation -- "sends prompt" --> ContentGenerator; - ContentGenerator -- "returns A2UI messages" --> GenUiConversation; - GenUiConversation -- "handles messages" --> A2uiMessageProcessor; - A2uiMessageProcessor -- "notifies of updates" --> GenUiSurface; - GenUiSurface -- "renders UI" --> UserInteraction; - UserInteraction -- "creates event" --> GenUiSurface; - GenUiSurface -- "sends event to host" --> A2uiMessageProcessor; - A2uiMessageProcessor -- "sends user input to" --> GenUiConversation; + UserInput -- "calls sendRequest()" --> Conversation; + Conversation -- "calls onSend" --> ExternalLLM[External LLM]; + ExternalLLM -- "returns chunks" --> Transport; + Transport -- "pipes to" --> Transformer; + Transformer -- "parses events" --> Transport; + Transport -- "streams messages" --> SurfaceController; + SurfaceController -- "updates state" --> Surface; + Surface -- "renders UI" --> UserInteraction; + UserInteraction -- "creates event" --> SurfaceController; + SurfaceController -- "emits submit" --> Conversation; + Conversation -- "loops back" --> ExternalLLM; ``` See [DESIGN.md](./DESIGN.md) for more detailed information about the design. @@ -82,46 +87,11 @@ running `flutter create`. ### 2. Configure your agent provider -`genui` can connect to a variety of agent providers. Choose the section -below for your preferred provider. - -#### Configure Firebase AI Logic - -To use the built-in `FirebaseAiContentGenerator` to connect to Gemini via Firebase AI -Logic, follow these instructions: - -1. [Create a new Firebase project](https://support.google.com/appsheet/answer/10104995) - using the Firebase Console. -2. [Enable the Gemini API](https://firebase.google.com/docs/gemini-in-firebase/set-up-gemini) - for that project. -3. Follow the first three steps in - [Firebase's Flutter Setup guide](https://firebase.google.com/docs/flutter/setup) - to add Firebase to your app. -4. Use `flutter pub add` to add the `genui` and `genui_firebase_ai` packages as - dependencies in your `pubspec.yaml` file: - - ```bash - flutter pub add genui genui_firebase_ai - ``` - -5. In your app's `main` method, ensure that the widget bindings are initialized, - and then initialize Firebase. - - ```dart - void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - runApp(const MyApp()); - } - ``` - -#### Configure another agent provider +`genui` is backend-agnostic and can connect to any agent provider. You simply need to implement the `onSend` callback in `A2uiTransportAdapter` to bridge your AI service to the framework. -To use `genui` with another agent provider, you need to follow that -provider's instructions to configure your app, and then create your own subclass -of `ContentGenerator` to connect to that provider. Use `FirebaseAiContentGenerator` or -`A2uiContentGenerator` (from the `genui_a2ui` package) as examples -of how to do so. +1. **Implement `onSend`**: This callback takes a `ChatMessage` and returns a `Future`. +2. **Call your AI Service**: Use your preferred AI client (e.g., `google_generative_ai`, `firebase_vertexai`, or a custom HTTP client) to send the message. +3. **Pipe Results**: As chunks of text arrive from the AI stream, feed them into `A2uiTransportAdapter.addChunk()`. ### 3. Create the connection to an agent @@ -140,54 +110,62 @@ requests: Next, use the following instructions to connect your app to your chosen agent provider. -1. Create a `A2uiMessageProcessor`, and provide it with the catalog of widgets you want - to make available to the agent. -2. Create a `ContentGenerator`, and provide it with a system instruction and a set of - tools (functions you want the agent to be able to invoke). You should always - include those provided by `A2uiMessageProcessor`, but feel free to include others. -3. Create a `GenUiConversation` using the instances of `ContentGenerator` and `A2uiMessageProcessor`. Your - app will primarily interact with this object to get things done. +2. Create a `SurfaceController`. +3. Create an `A2uiTransportAdapter` to handle communication. +4. Create a `Conversation` using the `SurfaceController` and `A2uiTransportAdapter`. For example: ```dart class _MyHomePageState extends State { - late final A2uiMessageProcessor _a2uiMessageProcessor; - late final GenUiConversation _genUiConversation; + late final SurfaceController _controller; + late final A2uiTransportAdapter _transport; + late final Conversation _conversation; @override void initState() { super.initState(); - // Create a A2uiMessageProcessor with a widget catalog. - // The CoreCatalogItems contain basic widgets for text, markdown, and images. - _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [CoreCatalogItems.asCatalog()]); - - // Create a ContentGenerator to communicate with the LLM. - // Provide system instructions and the tools from the A2uiMessageProcessor. - final contentGenerator = FirebaseAiContentGenerator( - catalog: CoreCatalogItems.asCatalog(), - systemInstruction: ''' - You are an expert in creating funny riddles. Every time I give you a word, - you should generate UI that displays one new riddle related to that word. - Each riddle should have both a question and an answer. - ''', - ); + // Create a SurfaceController with a widget catalog. + _controller = SurfaceController(catalogs: [CoreCatalogItems.asCatalog()]); + + // Create transport adapter + _transport = A2uiTransportAdapter(onSend: _onSendToLLM); - // Create the GenUiConversation to orchestrate everything. - _genUiConversation = GenUiConversation( - a2uiMessageProcessor: _a2uiMessageProcessor, - contentGenerator: contentGenerator, - onSurfaceAdded: _onSurfaceAdded, // Added in the next step. - onSurfaceDeleted: _onSurfaceDeleted, // Added in the next step. + // Create the Conversation to orchestrate everything. + _conversation = Conversation( + controller: _controller, + transport: _transport, ); + + // Listen to conversation events + _conversation.events.listen((event) { + if (event is ConversationSurfaceAdded) { + _onSurfaceAdded(event); + } else if (event is ConversationSurfaceRemoved) { + _onSurfaceDeleted(event); + } + }); + } + + Future _onSendToLLM(ChatMessage message) async { + // Implement your LLM integration here. + // For example, if using an HTTP client: + // Note: history is now managed by the LLM client or passed if needed, + // but typical adapters might just send the new message or the whole history. + // A2uiTransportAdapter.onSend signature is Future Function(ChatMessage message). + final responseStream = myLlmClient.streamGenerateContent(message); + await for (final chunk in responseStream) { + _transport.addChunk(chunk); + } } @override void dispose() { _textController.dispose(); - _genUiConversation.dispose(); - + _conversation.dispose(); + _transport.dispose(); + _controller.dispose(); super.dispose(); } } @@ -195,14 +173,14 @@ provider. ### 4. Send messages and display the agent's responses -Send a message to the agent using the `sendRequest` method in the `GenUiConversation` +Send a message to the agent using the `sendRequest` method in the `Conversation` class. To receive and display generated UI: -1. Use `GenUiConversation`'s callbacks to track the addition and removal of UI surfaces as +1. Use `Conversation`'s callbacks to track the addition and removal of UI surfaces as they are generated. These events include a "surface ID" for each surface. -2. Build a `GenUiSurface` widget for each active surface using the surface IDs +2. Build a `Surface` widget for each active surface using the surface IDs received in the previous step. For example: @@ -218,11 +196,11 @@ To receive and display generated UI: // Send a message containing the user's text to the agent. void _sendMessage(String text) { if (text.trim().isEmpty) return; - _genUiConversation.sendRequest(UserMessage.text(text)); + _conversation.sendRequest(ChatMessage.user(text)); } - // A callback invoked by the [GenUiConversation] when a new UI surface is generated. - // Here, the ID is stored so the build method can create a GenUiSurface to + // A callback invoked by the [Conversation] when a new UI surface is generated. + // Here, the ID is stored so the build method can create a Surface to // display it. void _onSurfaceAdded(SurfaceAdded update) { setState(() { @@ -230,8 +208,8 @@ To receive and display generated UI: }); } - // A callback invoked by GenUiConversation when a UI surface is removed. - void _onSurfaceDeleted(SurfaceRemoved update) { + // A callback invoked by Conversation when a UI surface is removed. + void _onSurfaceRemoved(SurfaceRemoved update) { setState(() { _surfaceIds.remove(update.surfaceId); }); @@ -250,9 +228,9 @@ To receive and display generated UI: child: ListView.builder( itemCount: _surfaceIds.length, itemBuilder: (context, index) { - // For each surface, create a GenUiSurface to display it. + // For each surface, create a Surface to display it. final id = _surfaceIds[index]; - return GenUiSurface(host: _genUiConversation.host, surfaceId: id); + return Surface(host: _conversation.host, surfaceId: id); }, ), ), @@ -376,30 +354,17 @@ final riddleCard = CatalogItem( #### Add the `CatalogItem` to the catalog -Include your catalog items when instantiating `A2uiMessageProcessor`. +Include your catalog items when instantiating `SurfaceController`. ```dart -_a2uiMessageProcessor = A2uiMessageProcessor( +_controller = SurfaceController( catalogs: [CoreCatalogItems.asCatalog().copyWith([riddleCard])], ); ``` #### Update the system instruction to use the new widget -In order to make sure the agent knows to use your new widget, use the system -instruction to explicitly tell it how and when to do so. Provide the name from -the CatalogItem when you do. - -```dart -final contentGenerator = FirebaseAiContentGenerator( - systemInstruction: ''' - You are an expert in creating funny riddles. Every time I give you a word, - you should generate a RiddleCard that displays one new riddle related to that word. - Each riddle should have both a question and an answer. - ''', - tools: _a2uiMessageProcessor.getTools(), -); -``` +In order to make sure the agent knows to use your new widget, usage of the prompt engineering techniques is required (e.g. one-shot or few-shot prompting) to explicitly tell it how and when to do so. Provide the name from the CatalogItem when you do. ### Data Model and Data Binding @@ -409,16 +374,14 @@ Widgets are "bound" to data in this model. When data in the model changes, only #### Binding to the Data Model -To bind a widget's property to the data model, you use a special JSON object in the data sent from the AI. This object can contain either a `literalString` (for static values) or a `path` (to bind to a value in the data model). +To bind a widget's property to the data model, you use a special JSON object in the data sent from the AI. Properties can be either a direct value (for static values) or a `path` object (to bind to a value in the data model). For example, to display a user's name in a `Text` widget, the AI would generate: ```json { "Text": { - "text": { - "literalString": "Welcome to GenUI" - }, + "text": "Welcome to GenUI", "hint": "h1" } } @@ -429,9 +392,7 @@ For example, to display a user's name in a `Text` widget, the AI would generate: ```json { "Image": { - "url": { - "literalString": "https://example.com/image.png" - }, + "url": "https://example.com/image.png", "hint": "mediumFeature" } } @@ -452,13 +413,6 @@ Check out the [examples](../../examples) included in this repo! The If something is unclear or missing, please [create an issue](https://github.com/flutter/genui/issues/new/choose). -### System instructions - -The `genui` package gives the LLM a set of tools it can use to generate -UI. To get the LLM to use these tools, the `systemInstruction` provided to -`ContentGenerator` must explicitly tell it to do so. This is why the previous example -includes a system instruction for the agent with the line "Every time I give -you a word, you should generate UI that displays one new riddle...". ### Troubleshooting / FAQ @@ -471,7 +425,7 @@ To observe communication between your app and the agent, enable logging in your import 'package:logging/logging.dart'; import 'package:genui/genui.dart'; -final logger = configureGenUiLogging(level: Level.ALL); +final logger = configureLogging(level: Level.ALL); void main() async { logger.onRecord.listen((record) { diff --git a/packages/genui/lib/genui.dart b/packages/genui/lib/genui.dart index 7a93d55a5..0457f9f26 100644 --- a/packages/genui/lib/genui.dart +++ b/packages/genui/lib/genui.dart @@ -9,26 +9,33 @@ /// data handling, and communication with a generative AI service. library; -export 'src/catalog/core_catalog.dart'; -export 'src/content_generator.dart'; -export 'src/core/a2ui_message_processor.dart'; -export 'src/core/genui_surface.dart'; -export 'src/core/prompt_fragments.dart'; -export 'src/core/ui_tools.dart'; -export 'src/core/widget_utilities.dart'; +export 'src/catalog/basic_catalog.dart'; export 'src/development_utilities/catalog_view.dart'; -export 'src/facade/direct_call_integration/model.dart'; -export 'src/facade/direct_call_integration/utils.dart'; -export 'src/facade/gen_ui_conversation.dart'; +export 'src/engine/surface_controller.dart'; +export 'src/engine/surface_registry.dart' show RegistryEvent; +export 'src/facade/conversation.dart'; +export 'src/facade/prompt_builder.dart'; export 'src/facade/widgets/chat_primitives.dart'; +export 'src/interfaces/a2ui_message_sink.dart'; +export 'src/interfaces/surface_context.dart'; +export 'src/interfaces/surface_host.dart'; +export 'src/interfaces/transport.dart'; export 'src/model/a2ui_client_capabilities.dart'; export 'src/model/a2ui_message.dart'; export 'src/model/a2ui_schemas.dart'; +export 'src/model/basic_catalog_embed.dart'; export 'src/model/catalog.dart'; export 'src/model/catalog_item.dart'; export 'src/model/chat_message.dart'; export 'src/model/data_model.dart'; -export 'src/model/tools.dart'; +export 'src/model/generation_events.dart'; export 'src/model/ui_models.dart'; +export 'src/primitives/cancellation.dart'; +export 'src/primitives/constants.dart'; export 'src/primitives/logging.dart'; export 'src/primitives/simple_items.dart'; +export 'src/transport/a2ui_parser_transformer.dart'; +export 'src/transport/a2ui_transport_adapter.dart'; +export 'src/widgets/fallback_widget.dart'; +export 'src/widgets/surface.dart'; +export 'src/widgets/widget_utilities.dart'; diff --git a/packages/genui_a2ui/example/ios/Runner/Runner-Bridging-Header.h b/packages/genui/lib/parsing.dart similarity index 56% rename from packages/genui_a2ui/example/ios/Runner/Runner-Bridging-Header.h rename to packages/genui/lib/parsing.dart index 02588e01d..ff8bbcc0b 100644 --- a/packages/genui_a2ui/example/ios/Runner/Runner-Bridging-Header.h +++ b/packages/genui/lib/parsing.dart @@ -2,4 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import "GeneratedPluginRegistrant.h" +/// Utilities for parsing JSON blocks from text streams. +library; + +export 'src/utils/json_block_parser.dart'; diff --git a/packages/genui/lib/src/catalog/core_catalog.dart b/packages/genui/lib/src/catalog/basic_catalog.dart similarity index 69% rename from packages/genui/lib/src/catalog/core_catalog.dart rename to packages/genui/lib/src/catalog/basic_catalog.dart index 66481ad64..905a2f4fe 100644 --- a/packages/genui/lib/src/catalog/core_catalog.dart +++ b/packages/genui/lib/src/catalog/basic_catalog.dart @@ -5,29 +5,29 @@ import '../model/catalog.dart'; import '../model/catalog_item.dart'; import '../primitives/constants.dart'; -import 'core_widgets/audio_player.dart' as audio_player_item; -import 'core_widgets/button.dart' as button_item; -import 'core_widgets/card.dart' as card_item; -import 'core_widgets/check_box.dart' as check_box_item; -import 'core_widgets/column.dart' as column_item; -import 'core_widgets/date_time_input.dart' as date_time_input_item; -import 'core_widgets/divider.dart' as divider_item; -import 'core_widgets/icon.dart' as icon_item; -import 'core_widgets/image.dart' as image_item; -import 'core_widgets/list.dart' as list_item; -import 'core_widgets/modal.dart' as modal_item; -import 'core_widgets/multiple_choice.dart' as multiple_choice_item; -import 'core_widgets/row.dart' as row_item; -import 'core_widgets/slider.dart' as slider_item; -import 'core_widgets/tabs.dart' as tabs_item; -import 'core_widgets/text.dart' as text_item; -import 'core_widgets/text_field.dart' as text_field_item; -import 'core_widgets/video.dart' as video_item; - -/// A collection of standard catalog items that can be used to build simple +import 'basic_catalog_widgets/audio_player.dart' as audio_player_item; +import 'basic_catalog_widgets/button.dart' as button_item; +import 'basic_catalog_widgets/card.dart' as card_item; +import 'basic_catalog_widgets/check_box.dart' as check_box_item; +import 'basic_catalog_widgets/choice_picker.dart' as choice_picker_item; +import 'basic_catalog_widgets/column.dart' as column_item; +import 'basic_catalog_widgets/date_time_input.dart' as date_time_input_item; +import 'basic_catalog_widgets/divider.dart' as divider_item; +import 'basic_catalog_widgets/icon.dart' as icon_item; +import 'basic_catalog_widgets/image.dart' as image_item; +import 'basic_catalog_widgets/list.dart' as list_item; +import 'basic_catalog_widgets/modal.dart' as modal_item; +import 'basic_catalog_widgets/row.dart' as row_item; +import 'basic_catalog_widgets/slider.dart' as slider_item; +import 'basic_catalog_widgets/tabs.dart' as tabs_item; +import 'basic_catalog_widgets/text.dart' as text_item; +import 'basic_catalog_widgets/text_field.dart' as text_field_item; +import 'basic_catalog_widgets/video.dart' as video_item; + +/// A collection of basic catalog items that can be used to build simple /// interactive UIs. -class CoreCatalogItems { - CoreCatalogItems._(); +abstract final class BasicCatalogItems { + BasicCatalogItems._(); /// Represents a UI element for playing audio content. /// @@ -65,13 +65,6 @@ class CoreCatalogItems { /// source. static final CatalogItem image = image_item.image; - /// Represents a UI element for displaying image data from a URL or other - /// source without letting the LLM determine the size. - /// - /// This is not included in the core catalog by default - instead it is a - /// variant of a core catalog item that can be included in custom catalogs. - static final CatalogItem imageFixedSize = image_item.imageFixedSize; - /// Represents a scrollable list of child widgets. /// /// Can be configured to lay out items linearly. @@ -85,7 +78,7 @@ class CoreCatalogItems { /// Represents a widget allowing the user to select one or more options from a /// list. - static final CatalogItem multipleChoice = multiple_choice_item.multipleChoice; + static final CatalogItem choicePicker = choice_picker_item.choicePicker; /// Represents a layout widget that arranges its children in a horizontal /// sequence. @@ -123,13 +116,13 @@ class CoreCatalogItems { image, list, modal, - multipleChoice, + choicePicker, row, slider, tabs, text, textField, video, - ], catalogId: standardCatalogId); + ], catalogId: basicCatalogId); } } diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/audio_player.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/audio_player.dart new file mode 100644 index 000000000..42b209355 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/audio_player.dart @@ -0,0 +1,66 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['AudioPlayer']), + 'url': A2uiSchemas.stringReference( + description: 'The URL of the audio to play.', + ), + 'description': A2uiSchemas.stringReference( + description: 'A description of the audio, such as a title or summary.', + ), + }, + required: ['component', 'url'], +); + +/// A catalog item for an audio player. +/// +/// This widget displays a placeholder for an audio player, used to represent +/// a component capable of playing audio from a given URL. +/// +/// ## Parameters: +/// +/// - `url`: The URL of the audio to play. +final audioPlayer = CatalogItem( + name: 'AudioPlayer', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final Object? description = (itemContext.data as JsonMap)['description']; + final ValueNotifier descriptionNotifier = itemContext.dataContext + .subscribeToString(description); + + return ValueListenableBuilder( + valueListenable: descriptionNotifier, + builder: (context, description, child) { + return Semantics( + label: description, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200, maxHeight: 100), + child: const Placeholder(child: Center(child: Text('AudioPlayer'))), + ), + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "AudioPlayer", + "url": "https://example.com/audio.mp3" + } + ] + ''', + ], +); diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart new file mode 100644 index 000000000..5d3c36966 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/button.dart @@ -0,0 +1,231 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../functions/expression_parser.dart'; +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../model/ui_models.dart'; +import '../../primitives/logging.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; +import 'widget_helpers.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['Button']), + 'child': A2uiSchemas.componentReference( + description: + 'The ID of a child widget. This should always be set, e.g. to the ID ' + 'of a `Text` widget.', + ), + 'action': A2uiSchemas.action(), + 'variant': S.string( + description: 'A hint for the button style.', + enumValues: ['primary', 'borderless'], + ), + 'checks': A2uiSchemas.checkable(), + }, + required: ['component', 'child', 'action'], +); + +extension type _ButtonData.fromMap(JsonMap _json) { + factory _ButtonData({ + required String child, + required JsonMap action, + String? variant, + List? checks, + }) => _ButtonData.fromMap({ + 'child': child, + 'action': action, + 'variant': variant, + 'checks': checks, + }); + + String get child { + final Object? val = _json['child']; + if (val is String) return val; + throw ArgumentError('Invalid child: $val'); + } + + JsonMap get action => _json['action'] as JsonMap; + String? get variant => _json['variant'] as String?; + List? get checks => (_json['checks'] as List?)?.cast(); +} + +/// A catalog item representing a Material Design elevated button. +/// +/// This widget displays an interactive button. When pressed, it dispatches +/// the specified `action` event. The button's appearance can be styled as +/// a primary action. +/// +/// ## Parameters: +/// +/// - `child`: The ID of a child widget to display inside the button. +/// - `action`: The action to perform when the button is pressed. +/// - `variant`: A hint for the button style ('primary' or 'borderless'). +final button = CatalogItem( + name: 'Button', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final buttonData = _ButtonData.fromMap(itemContext.data as JsonMap); + final Widget child = itemContext.buildChild(buttonData.child); + genUiLogger.info('Building Button with child: ${buttonData.child}'); + final ColorScheme colorScheme = Theme.of( + itemContext.buildContext, + ).colorScheme; + final String variant = buttonData.variant ?? ''; + final primary = variant == 'primary'; + final borderless = variant == 'borderless'; + + final TextStyle? textStyle = Theme.of(itemContext.buildContext) + .textTheme + .bodyLarge + ?.copyWith( + color: primary ? colorScheme.onPrimary : colorScheme.onSurface, + ); + + final ButtonStyle style = switch (variant) { + 'primary' => ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + ), + 'borderless' => TextButton.styleFrom( + foregroundColor: colorScheme.onSurface, + ), + _ => ElevatedButton.styleFrom( + backgroundColor: colorScheme.surface, + foregroundColor: colorScheme.onSurface, + ), + }; + + // Validate checks to determine if button is enabled + return ValueListenableBuilder( + valueListenable: itemContext.dataContext.createComputedNotifier( + checksToExpression(buttonData.checks), + ), + builder: (context, isValid, _) { + final enabled = isValid != false; // Default to true if null (no checks) + + final Widget buttonWidget = borderless + ? TextButton( + onPressed: enabled + ? () => _handlePress(itemContext, buttonData) + : null, + child: child, + ) + : ElevatedButton( + style: style.copyWith( + textStyle: WidgetStatePropertyAll(textStyle), + ), + onPressed: enabled + ? () => _handlePress(itemContext, buttonData) + : null, + child: child, + ); + + return buttonWidget; + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Button", + "child": "text", + "action": { + "event": { + "name": "button_pressed" + } + } + }, + { + "id": "text", + "component": "Text", + "text": "Hello World" + } + ] + ''', + () => ''' + [ + { + "id": "root", + "component": "Column", + "children": ["primaryButton", "secondaryButton"] + }, + { + "id": "primaryButton", + "component": "Button", + "child": "primaryText", + "primary": true, + "action": { + "event": { + "name": "primary_pressed" + } + } + }, + { + "id": "secondaryButton", + "component": "Button", + "child": "secondaryText", + "action": { + "event": { + "name": "secondary_pressed" + } + } + }, + { + "id": "primaryText", + "component": "Text", + "text": "Primary Button" + }, + { + "id": "secondaryText", + "component": "Text", + "text": "Secondary Button" + } + ] + ''', + ], +); + +void _handlePress(CatalogItemContext itemContext, _ButtonData buttonData) { + final JsonMap actionData = buttonData.action; + if (actionData.containsKey('event')) { + final eventMap = actionData['event'] as JsonMap; + final actionName = eventMap['name'] as String; + final contextDefinition = eventMap['context'] as JsonMap?; + + final JsonMap resolvedContext = resolveContext( + itemContext.dataContext, + contextDefinition, + ); + itemContext.dispatchEvent( + UserActionEvent( + name: actionName, + sourceComponentId: itemContext.id, + context: resolvedContext, + ), + ); + } else if (actionData.containsKey('functionCall')) { + final funcMap = actionData['functionCall'] as JsonMap; + final callName = funcMap['call'] as String; + + if (callName == 'closeModal') { + Navigator.of(itemContext.buildContext).pop(); + return; + } + + final parser = ExpressionParser(itemContext.dataContext); + parser.evaluateFunctionCall(funcMap); + } else { + genUiLogger.warning( + 'Button action missing event or functionCall: $actionData', + ); + } +} diff --git a/packages/genui/lib/src/catalog/core_widgets/card.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/card.dart similarity index 77% rename from packages/genui/lib/src/catalog/core_widgets/card.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/card.dart index 9320d9b21..c698cab8a 100644 --- a/packages/genui/lib/src/catalog/core_widgets/card.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/card.dart @@ -10,15 +10,22 @@ import '../../model/catalog_item.dart'; import '../../primitives/simple_items.dart'; final _schema = S.object( - properties: {'child': A2uiSchemas.componentReference()}, - required: ['child'], + properties: { + 'component': S.string(enumValues: ['Card']), + 'child': A2uiSchemas.componentReference(), + }, + required: ['component', 'child'], ); extension type _CardData.fromMap(JsonMap _json) { factory _CardData({required String child}) => _CardData.fromMap({'child': child}); - String get child => _json['child'] as String; + String get child { + final Object? val = _json['child']; + if (val is String) return val; + throw ArgumentError('Invalid child: $val'); + } } /// A catalog item representing a Material Design card. @@ -48,21 +55,13 @@ final card = CatalogItem( [ { "id": "root", - "component": { - "Card": { - "child": "text" - } - } + "component": "Card", + "child": "text" }, { "id": "text", - "component": { - "Text": { - "text": { - "literalString": "This is a card." - } - } - } + "component": "Text", + "text": "This is a card." } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/check_box.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/check_box.dart new file mode 100644 index 000000000..439fd9567 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/check_box.dart @@ -0,0 +1,125 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; +import 'widget_helpers.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['CheckBox']), + 'label': A2uiSchemas.stringReference(), + 'value': A2uiSchemas.booleanReference(), + 'checks': A2uiSchemas.checkable(), + }, + required: ['component', 'label', 'value'], +); + +extension type _CheckBoxData.fromMap(JsonMap _json) { + factory _CheckBoxData({ + required JsonMap label, + required JsonMap value, + List? checks, + }) => + _CheckBoxData.fromMap({'label': label, 'value': value, 'checks': checks}); + + Object get label => _json['label'] as Object; + Object get value => _json['value'] as Object; + List? get checks => (_json['checks'] as List?)?.cast(); +} + +/// A catalog item representing a Material Design checkbox with a label. +/// +/// This widget displays a checkbox a [Text] label. The checkbox's state +/// is bidirectionally bound to the data model path specified in the `value` +/// parameter. +/// +/// ## Parameters: +/// +/// - `label`: The text to display next to the checkbox. +/// - `value`: The boolean value of the checkbox. +final checkBox = CatalogItem( + name: 'CheckBox', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final checkBoxData = _CheckBoxData.fromMap(itemContext.data as JsonMap); + final ValueNotifier labelNotifier = itemContext.dataContext + .subscribeToString(checkBoxData.label); + + final Object valueRef = checkBoxData.value; + final path = (valueRef is Map && valueRef.containsKey('path')) + ? valueRef['path'] as String + : '${itemContext.id}.value'; + + final ValueNotifier valueNotifier = itemContext.dataContext + .subscribeToBool({'path': path}); + + return ValueListenableBuilder( + valueListenable: labelNotifier, + builder: (context, label, child) { + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, child) { + final bool effectiveValue = + value ?? (valueRef is bool ? valueRef : false); + + // Wrap the checkbox in validation + return ValueListenableBuilder( + valueListenable: itemContext.dataContext.createComputedNotifier( + checksToExpression(checkBoxData.checks), + ), + builder: (context, isValid, _) { + final isError = isValid == false; + + final Widget checkboxWidget = ListTileTheme.merge( + child: CheckboxListTile( + title: Text(label ?? ''), + value: effectiveValue, + onChanged: (newValue) { + if (newValue != null) { + itemContext.dataContext.update(path, newValue); + } + }, + subtitle: isError + ? Text( + 'Invalid value', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ) + : null, + isError: isError, + ), + ); + + return checkboxWidget; + }, + ); + }, + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "CheckBox", + "label": "Check me", + "value": { + "path": "/myValue" + } + } + ] + ''', + ], + isImplicitlyFlexible: true, +); diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/choice_picker.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/choice_picker.dart new file mode 100644 index 000000000..a4351a363 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/choice_picker.dart @@ -0,0 +1,384 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; +import 'widget_helpers.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['ChoicePicker']), + 'label': A2uiSchemas.stringReference( + description: 'The label for the group of options.', + ), + 'options': A2uiSchemas.listOrReference( + description: 'The list of available options to choose from.', + items: S.object( + properties: { + 'label': A2uiSchemas.stringReference( + description: 'The text to display for this option.', + ), + 'value': S.string( + description: 'The stable value associated with this option.', + ), + }, + required: ['label', 'value'], + ), + ), + 'value': S.combined( + oneOf: [ + S.string(), + S.list(items: S.string()), + A2uiSchemas.dataBindingSchema(), + A2uiSchemas.functionCall(), + ], + description: 'The list of currently selected values (or single value).', + ), + 'displayStyle': S.string( + description: 'The display style of the component.', + enumValues: ['checkbox', 'chips'], + ), + 'variant': S.string( + description: + 'A hint for how the choice picker should be displayed and behave.', + enumValues: ['multipleSelection', 'mutuallyExclusive'], + ), + 'filterable': S.boolean( + description: 'Whether the options can be filtered by the user.', + ), + 'checks': A2uiSchemas.checkable(), + }, + required: ['component', 'options', 'value'], +); + +extension type _ChoicePickerData.fromMap(JsonMap _json) { + Object? get label => _json['label']; + String? get variant => _json['variant'] as String?; + Object? get options => _json['options']; + Object get value => _json['value'] as Object; + String? get displayStyle => _json['displayStyle'] as String?; + bool get filterable => _json['filterable'] as bool? ?? false; + List? get checks => (_json['checks'] as List?)?.cast(); +} + +/// A component that allows selecting one or more options from a list. +final choicePicker = CatalogItem( + name: 'ChoicePicker', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final data = _ChoicePickerData.fromMap(itemContext.data as JsonMap); + + final Object valueRef = data.value; + final path = (valueRef is Map && valueRef.containsKey('path')) + ? valueRef['path'] as String + : '${itemContext.id}.value'; + + itemContext.dataContext.subscribe(path); + + final isMutuallyExclusive = data.variant == 'mutuallyExclusive'; + final isChips = data.displayStyle == 'chips'; + + final Object? optionsRef = data.options; + final ValueNotifier?> optionsNotifier; + if (optionsRef is Map && optionsRef.containsKey('path')) { + optionsNotifier = itemContext.dataContext.subscribe>( + optionsRef['path'] as String, + ); + } else { + optionsNotifier = ValueNotifier(optionsRef as List?); + } + + // Wrap the picker in validation + return ValueListenableBuilder( + valueListenable: itemContext.dataContext.createComputedNotifier( + checksToExpression(data.checks), + ), + builder: (context, isValid, _) { + final isError = isValid == false; + + final Widget pickerWidget = _ChoicePicker( + label: data.label, + optionsNotifier: optionsNotifier, + valueRef: valueRef, + path: path, + itemContext: itemContext, + isMutuallyExclusive: isMutuallyExclusive, + isChips: isChips, + filterable: data.filterable, + ); + + if (!isError) { + return pickerWidget; + } + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + pickerWidget, + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 4.0), + child: Text( + 'Invalid selection', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ], + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Column", + "children": ["heading1", "radio", "heading2", "check"] + }, + { "id": "heading1", "component": "Text", "text": "Mutually Exclusive", "variant": "h4" }, + { "id": "radio", "component": "ChoicePicker", "variant": "mutuallyExclusive", "label": "Choose one", "value": ["A"], "options": [ { "label": "A", "value": "A" }, { "label": "B", "value": "B" } ] }, + { "id": "heading2", "component": "Text", "text": "Multiple Selection", "variant": "h4" }, + { "id": "check", "component": "ChoicePicker", "variant": "multipleSelection", "label": "Choose many", "value": { "path": "/multi" }, "options": [ { "label": "X", "value": "X" }, { "label": "Y", "value": "Y" } ] } + ] + ''', + ], + isImplicitlyFlexible: true, +); + +class _ChoicePicker extends StatefulWidget { + const _ChoicePicker({ + required this.label, + required this.optionsNotifier, + required this.valueRef, + required this.path, + required this.itemContext, + required this.isMutuallyExclusive, + required this.isChips, + required this.filterable, + }); + + final Object? label; + final ValueNotifier?> optionsNotifier; + final Object valueRef; + final String path; + final CatalogItemContext itemContext; + final bool isMutuallyExclusive; + final bool isChips; + final bool filterable; + + @override + State<_ChoicePicker> createState() => _ChoicePickerState(); +} + +class _ChoicePickerState extends State<_ChoicePicker> { + String _filter = ''; + late final ValueNotifier _selectionsNotifier; + + @override + void initState() { + super.initState(); + _selectionsNotifier = widget.itemContext.dataContext.subscribe( + widget.path, + ); + } + + @override + Widget build(BuildContext context) { + // Filtering is handled in the build method of the options. + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.label != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 16.0), + child: ValueListenableBuilder( + valueListenable: widget.itemContext.dataContext.subscribeToString( + widget.label!, + ), + builder: (context, label, child) { + if (label == null || label.isEmpty) { + return const SizedBox.shrink(); + } + return Text( + label, + style: Theme.of(context).textTheme.titleMedium, + ); + }, + ), + ), + if (widget.filterable) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + decoration: const InputDecoration( + hintText: 'Filter options', + prefixIcon: Icon(Icons.search), + ), + onChanged: (value) { + setState(() { + _filter = value; + }); + }, + ), + ), + ValueListenableBuilder( + valueListenable: _selectionsNotifier, + builder: (context, currentSelections, child) { + var effectiveSelections = currentSelections; + if (effectiveSelections == null) { + if (widget.valueRef is List) { + effectiveSelections = widget.valueRef; + } else if (widget.valueRef is String) { + effectiveSelections = [widget.valueRef]; + } + } else if (effectiveSelections is! List) { + effectiveSelections = [effectiveSelections]; + } + final List currentStrings = + (effectiveSelections as List?) + ?.map((e) => e.toString()) + .toList() ?? + []; + + return ValueListenableBuilder?>( + valueListenable: widget.optionsNotifier, + builder: (context, options, child) { + if (options == null) { + return const SizedBox.shrink(); + } + final List castOptions = options.cast(); + final List optionWidgets = []; + + for (final option in castOptions) { + final ValueNotifier labelNotifier = widget + .itemContext + .dataContext + .subscribeToString(option['label']); + final optionValue = option['value'] as String; + + optionWidgets.add( + ValueListenableBuilder( + valueListenable: labelNotifier, + builder: (context, label, child) { + if (widget.filterable && + _filter.isNotEmpty && + label != null && + !label.toLowerCase().contains( + _filter.toLowerCase(), + )) { + return const SizedBox.shrink(); + } + + if (widget.isChips) { + final bool selected = currentStrings.contains( + optionValue, + ); + return Padding( + padding: const EdgeInsets.all(4.0), + child: FilterChip( + label: Text(label ?? ''), + selected: selected, + onSelected: (bool selected) { + _updateSelection( + selected, + optionValue, + currentStrings, + ); + }, + ), + ); + } + + if (widget.isMutuallyExclusive) { + final Object? groupValue = currentStrings.isNotEmpty + ? currentStrings.first + : null; + + return RadioListTile( + controlAffinity: ListTileControlAffinity.leading, + dense: true, + title: Text(label ?? ''), + value: optionValue, + // ignore: deprecated_member_use + groupValue: groupValue is String + ? groupValue + : null, + // ignore: deprecated_member_use + onChanged: (newValue) { + if (newValue == null) return; + widget.itemContext.dataContext.update( + widget.path, + [newValue], + ); + }, + ); + } else { + return CheckboxListTile( + title: Text(label ?? ''), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + value: currentStrings.contains(optionValue), + onChanged: (newValue) { + _updateSelection( + newValue == true, + optionValue, + currentStrings, + ); + }, + ); + } + }, + ), + ); + } + + if (widget.isChips) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Wrap(children: optionWidgets), + ); + } + + return Column(children: optionWidgets); + }, + ); + }, + ), + ], + ); + } + + void _updateSelection( + bool selected, + String optionValue, + List currentStrings, + ) { + if (widget.isMutuallyExclusive) { + if (selected) { + widget.itemContext.dataContext.update(widget.path, [optionValue]); + } + } else { + final newSelections = List.from(currentStrings); + if (selected) { + if (!newSelections.contains(optionValue)) { + newSelections.add(optionValue); + } + } else { + newSelections.remove(optionValue); + } + widget.itemContext.dataContext.update(widget.path, newSelections); + } + } +} diff --git a/packages/genui/lib/src/catalog/core_widgets/column.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart similarity index 53% rename from packages/genui/lib/src/catalog/core_widgets/column.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart index 8c00e9492..6e6acc48d 100644 --- a/packages/genui/lib/src/catalog/core_widgets/column.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/column.dart @@ -3,18 +3,18 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -// ignore_for_file: avoid_dynamic_calls import 'package:json_schema_builder/json_schema_builder.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; +import '../../model/ui_models.dart'; import '../../primitives/simple_items.dart'; import 'widget_helpers.dart'; final _schema = S.object( properties: { - 'distribution': S.string( + 'component': S.string(enumValues: ['Column']), + 'justify': S.string( description: 'How children are aligned on the main axis. ', enumValues: [ 'start', @@ -23,11 +23,12 @@ final _schema = S.object( 'spaceBetween', 'spaceAround', 'spaceEvenly', + 'stretch', // Added stretch to match keys ], ), - 'alignment': S.string( + 'align': S.string( description: 'How children are aligned on the cross axis. ', - enumValues: ['start', 'center', 'end', 'stretch', 'baseline'], + enumValues: ['start', 'center', 'end', 'stretch'], ), 'children': A2uiSchemas.componentArrayReference( description: @@ -35,22 +36,20 @@ final _schema = S.object( 'template with a data binding to the list of children.', ), }, + required: ['component', 'children'], ); extension type _ColumnData.fromMap(JsonMap _json) { - factory _ColumnData({ - Object? children, - String? distribution, - String? alignment, - }) => _ColumnData.fromMap({ - 'children': children, - 'distribution': distribution, - 'alignment': alignment, - }); + factory _ColumnData({Object? children, String? justify, String? align}) => + _ColumnData.fromMap({ + 'children': children, + 'justify': justify, + 'align': align, + }); Object? get children => _json['children']; - String? get distribution => _json['distribution'] as String?; - String? get alignment => _json['alignment'] as String?; + String? get justify => _json['justify'] as String?; + String? get align => _json['align'] as String?; } MainAxisAlignment _parseMainAxisAlignment(String? alignment) { @@ -95,11 +94,11 @@ CrossAxisAlignment _parseCrossAxisAlignment(String? alignment) { /// /// ## Parameters: /// -/// - `distribution`: How the children should be placed along the main axis. Can +/// - `justify`: How the children should be placed along the main axis. Can /// be `start`, `center`, `end`, `spaceBetween`, `spaceAround`, or /// `spaceEvenly`. Defaults to `start`. -/// - `alignment`: How the children should be placed along the cross axis. Can -/// be `start`, `center`, `end`, `stretch`, or `baseline`. Defaults to +/// - `align`: How the children should be aligned on the cross axis. Can +/// be `start`, `center`, `end`, or `stretch`. Defaults to /// `start`. /// - `children`: A list of child widget IDs to display in the column. final column = CatalogItem( @@ -114,35 +113,74 @@ final column = CatalogItem( getComponent: itemContext.getComponent, explicitListBuilder: (childIds, buildChild, getComponent, dataContext) { return Column( - mainAxisAlignment: _parseMainAxisAlignment(columnData.distribution), - crossAxisAlignment: _parseCrossAxisAlignment(columnData.alignment), + mainAxisAlignment: _parseMainAxisAlignment(columnData.justify), + crossAxisAlignment: _parseCrossAxisAlignment(columnData.align), mainAxisSize: MainAxisSize.min, - children: childIds - .map( - (componentId) => buildWeightedChild( - componentId: componentId, - dataContext: dataContext, - buildChild: buildChild, - weight: getComponent(componentId)?.weight, - ), - ) - .toList(), + children: childIds.map((componentId) { + final explicitWeight = + getComponent(componentId)?.properties['weight'] as int?; + final bool isImplicitlyFlexible = + itemContext + .getCatalogItem(getComponent(componentId)?.type ?? '') + ?.isImplicitlyFlexible ?? + false; + final int? weight = + explicitWeight ?? (isImplicitlyFlexible ? 1 : null); + final FlexFit fit = explicitWeight != null + ? FlexFit.tight + : FlexFit.loose; + + return buildWeightedChild( + componentId: componentId, + dataContext: dataContext, + buildChild: buildChild, + weight: weight, + flexFit: fit, + ); + }).toList(), ); }, - templateListWidgetBuilder: (context, list, componentId, dataBinding) { + templateListWidgetBuilder: (context, data, componentId, dataBinding) { + final List values; + final List keys; + + if (data is List) { + values = data; + keys = List.generate(data.length, (index) => index.toString()); + } else if (data is Map) { + values = data.values.toList(); + keys = data.keys.map((k) => k.toString()).toList(); + } else { + return const SizedBox.shrink(); + } + + final Component? component = itemContext.getComponent(componentId); + final explicitWeight = component?.properties['weight'] as int?; + final bool isImplicitlyFlexible = + itemContext + .getCatalogItem(component?.type ?? '') + ?.isImplicitlyFlexible ?? + false; + final int? weight = explicitWeight ?? (isImplicitlyFlexible ? 1 : null); + final FlexFit fit = explicitWeight != null + ? FlexFit.tight + : FlexFit.loose; + return Column( - mainAxisAlignment: _parseMainAxisAlignment(columnData.distribution), - crossAxisAlignment: _parseCrossAxisAlignment(columnData.alignment), + mainAxisAlignment: _parseMainAxisAlignment(columnData.justify), + crossAxisAlignment: _parseCrossAxisAlignment(columnData.align), mainAxisSize: MainAxisSize.min, children: [ - for (var i = 0; i < list.length; i++) ...[ + for (var i = 0; i < values.length; i++) ...[ buildWeightedChild( componentId: componentId, dataContext: itemContext.dataContext.nested( - DataPath('$dataBinding/$i'), + '$dataBinding/${keys[i]}', ), buildChild: itemContext.buildChild, - weight: itemContext.getComponent(componentId)?.weight, + weight: weight, + flexFit: fit, + key: ValueKey(keys[i]), ), ], ], @@ -155,58 +193,37 @@ final column = CatalogItem( [ { "id": "root", - "component": { - "Column": { - "children": { - "explicitList": [ - "advice_text", - "advice_options", - "submit_button" - ] - } - } - } + "component": "Column", + "children": [ + "advice_text", + "advice_options", + "submit_button" + ] }, { "id": "advice_text", - "component": { - "Text": { - "text": { - "literalString": "What kind of advice are you looking for?" - } - } - } + "component": "Text", + "text": "What kind of advice are you looking for?" }, { "id": "advice_options", - "component": { - "Text": { - "text": { - "literalString": "Some advice options." - } - } - } + "component": "Text", + "text": "Some advice options." }, { "id": "submit_button", - "component": { - "Button": { - "child": "submit_button_text", - "action": { - "name": "submit" - } + "component": "Button", + "child": "submit_button_text", + "action": { + "event": { + "name": "submit" } } }, { "id": "submit_button_text", - "component": { - "Text": { - "text": { - "literalString": "Submit" - } - } - } + "component": "Text", + "text": "Submit" } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart new file mode 100644 index 000000000..a9e4e914d --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/date_time_input.dart @@ -0,0 +1,374 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../functions/expression_parser.dart'; +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../model/data_model.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['DateTimeInput']), + 'value': A2uiSchemas.stringReference( + description: 'The selected date and/or time.', + ), + 'variant': S.string( + description: 'The input type: date, time, or datetime.', + enumValues: ['date', 'time', 'datetime'], + ), + 'min': S.string( + description: + 'The earliest selectable date (YYYY-MM-DD). Defaults to -9999-01-01.', + ), + 'max': S.string( + description: + 'The latest selectable date (YYYY-MM-DD). Defaults to 9999-12-31.', + ), + 'label': A2uiSchemas.stringReference( + description: 'The text label for the input field.', + ), + 'checks': S.list(items: A2uiSchemas.validationCheck()), + }, + required: ['component', 'value'], +); + +extension type _DateTimeInputData.fromMap(JsonMap _json) { + factory _DateTimeInputData({ + required JsonMap value, + String? variant, + String? min, + String? max, + Object? label, + List? checks, + }) => _DateTimeInputData.fromMap({ + 'value': value, + 'variant': variant, + 'min': min, + 'max': max, + 'label': label, + 'checks': checks, + }); + + Object get value => _json['value'] as Object; + String? get variant => _json['variant'] as String?; + Object? get label => _json['label']; + List? get checks => (_json['checks'] as List?)?.cast(); + + bool get enableDate { + final String? v = variant; + if (v == null) { + if (_json.containsKey('enableDate')) return _json['enableDate'] as bool; + return true; + } + return v == 'date' || v == 'datetime'; + } + + bool get enableTime { + final String? v = variant; + if (v == null) { + if (_json.containsKey('enableTime')) return _json['enableTime'] as bool; + return true; + } + return v == 'time' || v == 'datetime'; + } + + DateTime get firstDate => + DateTime.tryParse((_json['min'] as String?) ?? '') ?? DateTime(-9999); + DateTime get lastDate => + DateTime.tryParse((_json['max'] as String?) ?? '') ?? + DateTime(9999, 12, 31); +} + +class _DateTimeInput extends StatefulWidget { + const _DateTimeInput({ + required this.id, + required this.value, + required this.path, + required this.data, + required this.dataContext, + required this.onChanged, + this.label, + this.checks, + this.parser, + }); + + final String id; + final String? value; + final String path; + final _DateTimeInputData data; + final DataContext dataContext; + final VoidCallback onChanged; + final String? label; + final List? checks; + final ExpressionParser? parser; + + @override + State<_DateTimeInput> createState() => _DateTimeInputState(); +} + +class _DateTimeInputState extends State<_DateTimeInput> { + String? _errorText; + + @override + void initState() { + super.initState(); + _validate(); + } + + @override + void didUpdateWidget(_DateTimeInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != oldWidget.value || widget.checks != oldWidget.checks) { + _validate(); + } + } + + void _validate() { + final String? newError = _calculateError(); + if (newError != _errorText) { + setState(() { + _errorText = newError; + }); + } + } + + String? _calculateError() { + if (widget.checks == null || widget.parser == null) { + return null; + } + + for (final JsonMap check in widget.checks!) { + final Object? condition = check['condition']; + final bool isValid = widget.parser!.evaluateCondition(condition); + if (!isValid) { + return check['message'] as String? ?? 'Invalid value'; + } + } + return null; + } + + Future _handleTap(BuildContext context) async { + final DateTime initialDate = + DateTime.tryParse(widget.value ?? '') ?? + DateTime.tryParse('1970-01-01T${widget.value}') ?? + DateTime.now(); + + var resultDate = initialDate; + var resultTime = TimeOfDay.fromDateTime(initialDate); + + if (widget.data.enableDate) { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: widget.data.firstDate, + lastDate: widget.data.lastDate, + ); + if (pickedDate == null) { + return; + } + resultDate = pickedDate; + } + + if (widget.data.enableTime) { + final TimeOfDay? pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(initialDate), + ); + if (pickedTime == null) { + return; + } + resultTime = pickedTime; + } + + final finalDateTime = DateTime( + resultDate.year, + resultDate.month, + resultDate.day, + widget.data.enableTime ? resultTime.hour : 0, + widget.data.enableTime ? resultTime.minute : 0, + ); + + String formattedValue; + + if (widget.data.enableDate && !widget.data.enableTime) { + formattedValue = finalDateTime.toIso8601String().split('T').first; + } else if (!widget.data.enableDate && widget.data.enableTime) { + final String hour = finalDateTime.hour.toString().padLeft(2, '0'); + final String minute = finalDateTime.minute.toString().padLeft(2, '0'); + formattedValue = '$hour:$minute:00'; + } else { + formattedValue = finalDateTime.toIso8601String(); + } + + widget.dataContext.update(widget.path, formattedValue); + widget.onChanged(); + } + + String _getDisplayText(MaterialLocalizations localizations) { + if (widget.value == null) { + return _getPlaceholderText(); + } + + final DateTime? date = + DateTime.tryParse(widget.value!) ?? + DateTime.tryParse('1970-01-01T${widget.value}'); + + if (date == null) { + return widget.value!; + } + + final List parts = [ + if (widget.data.enableDate) localizations.formatFullDate(date), + if (widget.data.enableTime) + localizations.formatTimeOfDay(TimeOfDay.fromDateTime(date)), + ]; + return parts.join(' '); + } + + String _getPlaceholderText() { + if (widget.data.enableDate && widget.data.enableTime) { + return 'Select a date and time'; + } else if (widget.data.enableDate) { + return 'Select a date'; + } else if (widget.data.enableTime) { + return 'Select a time'; + } + return 'Select a date/time'; + } + + @override + Widget build(BuildContext context) { + final MaterialLocalizations localizations = MaterialLocalizations.of( + context, + ); + final String displayText = _getDisplayText(localizations); + + return InputDecorator( + decoration: InputDecoration( + labelText: widget.label, + errorText: _errorText, + border: const OutlineInputBorder(), + ), + child: InkWell( + onTap: () => _handleTap(context), + child: Text( + displayText, + key: Key('${widget.id}_text'), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ); + } +} + +/// A catalog item representing a Material Design date and/or time input field. +/// +/// This widget displays a field that, when tapped, opens the native date and/or +/// time pickers. The selected value is stored as a string in the data model +/// path specified by the `value` parameter. +/// +/// ## Parameters: +/// +/// - `value`: The selected date and/or time, as a string. +/// - `enableDate`: Whether to allow the user to select a date. Defaults to +/// `true`. +/// - `enableTime`: Whether to allow the user to select a time. Defaults to +/// `true`. +/// - `min`: The minimum allowed date. +/// - `max`: The maximum allowed date. +/// - `label`: The label text. +/// - `checks`: Validation checks. +final dateTimeInput = CatalogItem( + name: 'DateTimeInput', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final dateTimeInputData = _DateTimeInputData.fromMap( + itemContext.data as JsonMap, + ); + final Object valueRef = dateTimeInputData.value; + final path = (valueRef is Map && valueRef.containsKey('path')) + ? valueRef['path'] as String + : '${itemContext.id}.value'; + + final ValueNotifier valueNotifier = itemContext.dataContext + .subscribeToString({'path': path}); + final ValueNotifier labelNotifier = itemContext.dataContext + .subscribeToString(dateTimeInputData.label); + + final parser = ExpressionParser(itemContext.dataContext); + + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, child) { + var effectiveValue = value; + if (effectiveValue == null) { + final Object val = dateTimeInputData.value; + if (val is! Map || !val.containsKey('path')) { + effectiveValue = val as String?; + } + } + + return ValueListenableBuilder( + valueListenable: labelNotifier, + builder: (context, label, child) { + return _DateTimeInput( + id: itemContext.id, + value: effectiveValue, + path: path, + data: dateTimeInputData, + dataContext: itemContext.dataContext, + onChanged: () {}, + label: label, + checks: dateTimeInputData.checks, + parser: parser, + ); + }, + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "DateTimeInput", + "value": { + "path": "/myDateTime" + } + } + ] + ''', + () => ''' + [ + { + "id": "root", + "component": "DateTimeInput", + "value": { + "path": "/myDate" + }, + "enableTime": false + } + ] + ''', + () => ''' + [ + { + "id": "root", + "component": "DateTimeInput", + "value": { + "path": "/myTime" + }, + "enableDate": false + } + ] + ''', + ], + isImplicitlyFlexible: true, +); diff --git a/packages/genui/lib/src/catalog/core_widgets/divider.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/divider.dart similarity index 94% rename from packages/genui/lib/src/catalog/core_widgets/divider.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/divider.dart index 02e69426d..8a941f647 100644 --- a/packages/genui/lib/src/catalog/core_widgets/divider.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/divider.dart @@ -10,6 +10,7 @@ import '../../primitives/simple_items.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['Divider']), 'axis': S.string(enumValues: ['horizontal', 'vertical']), }, ); @@ -44,9 +45,7 @@ final divider = CatalogItem( [ { "id": "root", - "component": { - "Divider": {} - } + "component": "Divider" } ] ''', diff --git a/packages/genui/lib/src/catalog/core_widgets/icon.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/icon.dart similarity index 85% rename from packages/genui/lib/src/catalog/core_widgets/icon.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/icon.dart index 3d882ddaf..394eff1e6 100644 --- a/packages/genui/lib/src/catalog/core_widgets/icon.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/icon.dart @@ -7,28 +7,37 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; import '../../primitives/simple_items.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['Icon']), 'name': A2uiSchemas.stringReference( description: - '''The name of the icon to display. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/icon/name').''', + '''The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/icon/name').''', enumValues: AvailableIcons.allAvailable, ), }, - required: ['name'], + required: ['component', 'name'], ); extension type _IconData.fromMap(JsonMap _json) { - factory _IconData({required JsonMap name}) => + factory _IconData({required Object name}) => _IconData.fromMap({'name': name}); - JsonMap get nameMap => _json['name'] as JsonMap; + Object? get _name => _json['name']; - String? get literalName => nameMap['literalString'] as String?; - String? get namePath => nameMap['path'] as String?; + String? get literalName { + final Object? name = _name; + if (name is String) return name; + return null; + } + + String? get namePath { + final Object? name = _name; + if (name is JsonMap) return name['path'] as String?; + return null; + } } enum AvailableIcons { @@ -122,7 +131,7 @@ final icon = CatalogItem( } final ValueNotifier notifier = itemContext.dataContext - .subscribe(DataPath(namePath)); + .subscribe(namePath); return ValueListenableBuilder( valueListenable: notifier, @@ -139,13 +148,8 @@ final icon = CatalogItem( [ { "id": "root", - "component": { - "Icon": { - "name": { - "literalString": "add" - } - } - } + "component": "Icon", + "name": "add" } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart new file mode 100644 index 000000000..87efcd215 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/image.dart @@ -0,0 +1,179 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/logging.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; + +Schema _schema() { + final Map properties = { + 'component': S.string(enumValues: ['Image']), + 'url': A2uiSchemas.stringReference( + description: + 'Asset path (e.g. assets/...) or network URL (e.g. https://...)', + ), + 'fit': S.string( + description: 'How the image should be inscribed into the box.', + enumValues: BoxFit.values.map((e) => e.name).toList(), + ), + }; + properties['variant'] = S.string( + description: '''A hint for the image size and style. One of: + - icon: Small square icon. + - avatar: Circular avatar image. + - smallFeature: Small feature image. + - mediumFeature: Medium feature image. + - largeFeature: Large feature image. + - header: Full-width, full bleed, header image.''', + enumValues: [ + 'icon', + 'avatar', + 'smallFeature', + 'mediumFeature', + 'largeFeature', + 'header', + ], + ); + return S.object(properties: properties); +} + +extension type _ImageData.fromMap(JsonMap _json) { + factory _ImageData({required JsonMap url, String? fit, String? variant}) => + _ImageData.fromMap({'url': url, 'fit': fit, 'variant': variant}); + + Object get url => _json['url'] as Object; + BoxFit? get fit => _json['fit'] != null + ? BoxFit.values.firstWhere((e) => e.name == _json['fit'] as String) + : null; + String? get variant => _json['variant'] as String?; +} + +/// A catalog item representing a widget that displays an image. +/// +/// The image source is specified by the `url` parameter, which can be a network +/// URL (e.g., `https://...`) or a local asset path (e.g., `assets/...`). +/// +/// ## Parameters: +/// +/// - `url`: The URL of the image to display. Can be a network URL or a local +/// asset path. +/// - `fit`: How the image should be inscribed into the box. See [BoxFit] for +/// possible values. +/// - `variant`: A usage hint for the image size and style. One of 'icon', +/// 'avatar', 'smallFeature', 'mediumFeature', 'largeFeature', 'header'. +final CatalogItem image = CatalogItem( + name: 'Image', + dataSchema: _schema(), + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Image", + "url": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png", + "variant": "mediumFeature" + } + ] + ''', + ], + widgetBuilder: (itemContext) { + final imageData = _ImageData.fromMap(itemContext.data as JsonMap); + final ValueNotifier notifier = itemContext.dataContext + .subscribeToString(imageData.url); + + return ValueListenableBuilder( + valueListenable: notifier, + builder: (context, currentLocation, child) { + final location = currentLocation; + if (location == null || location.isEmpty) { + genUiLogger.warning( + 'Image widget created with no URL at path: ' + '${itemContext.dataContext.path}', + ); + return const SizedBox.shrink(); + } + final BoxFit? fit = imageData.fit; + final String? variant = imageData.variant; + + late Widget child; + + if (location.startsWith('assets/')) { + child = Image.asset( + location, + fit: fit, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.broken_image); + }, + ); + } else { + child = Image.network( + location, + fit: fit, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.broken_image); + }, + frameBuilder: + ( + BuildContext context, + Widget child, + int? frame, + bool wasSynchronouslyLoaded, + ) { + if (wasSynchronouslyLoaded) { + return child; + } + return AnimatedOpacity( + opacity: frame == null ? 0 : 1, + duration: const Duration(seconds: 1), + curve: Curves.easeOut, + child: child, + ); + }, + loadingBuilder: + ( + BuildContext context, + Widget child, + ImageChunkEvent? loadingProgress, + ) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ); + } + + if (variant == 'avatar') { + child = CircleAvatar(child: child); + } + + if (variant == 'header') { + return SizedBox(width: double.infinity, child: child); + } + + final double size = switch (variant) { + 'icon' || 'avatar' => 32.0, + 'smallFeature' => 50.0, + 'mediumFeature' => 150.0, + 'largeFeature' => 400.0, + _ => 150.0, + }; + + return SizedBox(width: size, height: size, child: child); + }, + ); + }, +); diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/list.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/list.dart new file mode 100644 index 000000000..0604381ba --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/list.dart @@ -0,0 +1,154 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../model/data_model.dart'; +import '../../primitives/logging.dart'; +import '../../primitives/simple_items.dart'; +import 'widget_helpers.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['List']), + 'children': A2uiSchemas.componentArrayReference(), + 'direction': S.string(enumValues: ['vertical', 'horizontal']), + 'align': S.string(enumValues: ['start', 'center', 'end', 'stretch']), + }, + required: ['component', 'children'], +); + +extension type _ListData.fromMap(JsonMap _json) { + factory _ListData({ + required Object? children, + String? direction, + String? align, + }) => _ListData.fromMap({ + 'children': children, + 'direction': direction, + 'align': align, + }); + + Object? get children => _json['children']; + String? get direction => _json['direction'] as String?; + String? get align => _json['align'] as String?; +} + +/// A catalog item representing a scrollable list of widgets. +/// +/// This widget is analogous to Flutter's [ListView] widget. It can display +/// children in either a vertical or horizontal direction. +/// +/// ## Parameters: +/// +/// - `children`: A list of child widget IDs to display in the list. +/// - `direction`: The direction of the list. Can be `vertical` or `horizontal`. +/// Defaults to `vertical`. +/// - `align`: The alignment of children along the cross axis. One of `start`, +/// `center`, `end`, `stretch`. +final list = CatalogItem( + name: 'List', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final listData = _ListData.fromMap(itemContext.data as JsonMap); + final Axis direction = listData.direction == 'horizontal' + ? Axis.horizontal + : Axis.vertical; + + final CrossAxisAlignment crossAxisAlignment = switch (listData.align) { + 'start' => CrossAxisAlignment.start, + 'center' => CrossAxisAlignment.center, + 'end' => CrossAxisAlignment.end, + 'stretch' => CrossAxisAlignment.stretch, + _ => CrossAxisAlignment.center, + }; + + Widget buildList(List children) { + return SingleChildScrollView( + scrollDirection: direction, + child: Flex( + direction: direction, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: crossAxisAlignment, + children: children, + ), + ); + } + + return ComponentChildrenBuilder( + childrenData: listData.children, + dataContext: itemContext.dataContext, + buildChild: itemContext.buildChild, + getComponent: itemContext.getComponent, + explicitListBuilder: (childIds, buildChild, getComponent, dataContext) { + return buildList( + childIds.map((id) { + return buildChild(id, dataContext); + }).toList(), + ); + }, + templateListWidgetBuilder: + (context, Object? data, componentId, dataBinding) { + final List values; + final List keys; + + if (data is List) { + values = data; + keys = List.generate(data.length, (index) => index.toString()); + } else if (data is Map) { + values = data.values.toList(); + keys = data.keys.map((k) => k.toString()).toList(); + } else { + genUiLogger.warning( + 'List: invalid data type for template list: ' + '${data.runtimeType}', + ); + return const SizedBox.shrink(); + } + + return buildList( + List.generate(values.length, (index) { + final nestedPath = '$dataBinding/${keys[index]}'; + + final DataContext itemDataContext = itemContext.dataContext + .nested(nestedPath); + final Widget child = itemContext.buildChild( + componentId, + itemDataContext, + ); + return KeyedSubtree(key: ValueKey(keys[index]), child: child); + }), + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "List", + "children": [ + "text1", + "text2" + ] + }, + { + "id": "text1", + "component": "Text", + "text": "First" + }, + { + "id": "text2", + "component": "Text", + "text": "Second" + } + ] + ''', + ], + isImplicitlyFlexible: true, +); diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/modal.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/modal.dart new file mode 100644 index 000000000..37dc9a2ec --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/modal.dart @@ -0,0 +1,103 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport '../../widgets/surface.dart'; +library; + +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../../genui.dart' show Surface; +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/surface.dart' show Surface; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['Modal']), + 'trigger': A2uiSchemas.componentReference( + description: 'The widget that opens the modal.', + ), + 'content': A2uiSchemas.componentReference( + description: 'The widget to display in the modal.', + ), + }, + required: ['component', 'trigger', 'content'], +); + +extension type _ModalData.fromMap(JsonMap _json) { + factory _ModalData({required String trigger, required String content}) => + _ModalData.fromMap({'trigger': trigger, 'content': content}); + + String get trigger { + final Object? val = _json['trigger']; + if (val is String) return val; + + if (val == null) { + return ''; + } + throw ArgumentError('Invalid trigger: $val'); + } + + String get content { + final Object? val = _json['content']; + if (val is String) return val; + throw ArgumentError('Invalid content: $val'); + } +} + +/// A catalog item representing a modal bottom sheet. +/// +/// This component doesn't render the modal content directly. Instead, it +/// renders the `trigger` widget. The `trigger` is expected to +/// trigger an action (e.g., on button press) that causes the `content` to +/// be displayed within a modal bottom sheet by the [Surface]. +/// +/// ## Parameters: +/// +/// - `trigger`: The ID of the widget that opens the modal. +/// - `content`: The ID of the widget to display in the modal. +final modal = CatalogItem( + name: 'Modal', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final modalData = _ModalData.fromMap(itemContext.data as JsonMap); + return itemContext.buildChild(modalData.trigger); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Modal", + "trigger": "button", + "content": "text" + }, + { + "id": "button", + "component": "Button", + "child": "button_text", + "action": { + "event": { + "name": "showModal", + "context": { + "modalId": "root" + } + } + } + }, + { + "id": "button_text", + "component": "Text", + "text": "Open Modal" + }, + { + "id": "text", + "component": "Text", + "text": "This is a modal." + } + ] + ''', + ], +); diff --git a/packages/genui/lib/src/catalog/core_widgets/row.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/row.dart similarity index 54% rename from packages/genui/lib/src/catalog/core_widgets/row.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/row.dart index 3190eb689..e820d7723 100644 --- a/packages/genui/lib/src/catalog/core_widgets/row.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/row.dart @@ -7,19 +7,19 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; import '../../model/ui_models.dart'; import '../../primitives/simple_items.dart'; import 'widget_helpers.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['Row']), 'children': A2uiSchemas.componentArrayReference( description: 'Either an explicit list of widget IDs for the children, or a ' 'template with a data binding to the list of children.', ), - 'distribution': S.string( + 'justify': S.string( enumValues: [ 'start', 'center', @@ -27,29 +27,25 @@ final _schema = S.object( 'spaceBetween', 'spaceAround', 'spaceEvenly', + 'stretch', ], ), - 'alignment': S.string( - enumValues: ['start', 'center', 'end', 'stretch', 'baseline'], - ), + 'align': S.string(enumValues: ['start', 'center', 'end', 'stretch']), }, - required: ['children'], + required: ['component', 'children'], ); extension type _RowData.fromMap(JsonMap _json) { - factory _RowData({ - Object? children, - String? distribution, - String? alignment, - }) => _RowData.fromMap({ - 'children': children, - 'distribution': distribution, - 'alignment': alignment, - }); + factory _RowData({Object? children, String? justify, String? align}) => + _RowData.fromMap({ + 'children': children, + 'justify': justify, + 'align': align, + }); Object? get children => _json['children']; - String? get distribution => _json['distribution'] as String?; - String? get alignment => _json['alignment'] as String?; + String? get justify => _json['justify'] as String?; + String? get align => _json['align'] as String?; } MainAxisAlignment _parseMainAxisAlignment(String? alignment) { @@ -81,8 +77,6 @@ CrossAxisAlignment _parseCrossAxisAlignment(String? alignment) { return CrossAxisAlignment.end; case 'stretch': return CrossAxisAlignment.stretch; - case 'baseline': - return CrossAxisAlignment.baseline; default: return CrossAxisAlignment.start; } @@ -97,11 +91,11 @@ CrossAxisAlignment _parseCrossAxisAlignment(String? alignment) { /// ## Parameters: /// /// - `children`: A list of child widget IDs to display in the row. -/// - `distribution`: How the children should be placed along the main axis. Can +/// - `justify`: How the children should be placed along the main axis. Can /// be `start`, `center`, `end`, `spaceBetween`, `spaceAround`, or /// `spaceEvenly`. Defaults to `start`. -/// - `alignment`: How the children should be placed along the cross axis. Can -/// be `start`, `center`, `end`, `stretch`, or `baseline`. Defaults to +/// - `align`: How the children should be aligned on the cross axis. Can +/// be `start`, `center`, `end`, or `stretch`. Defaults to /// `start`. final row = CatalogItem( name: 'Row', @@ -115,43 +109,75 @@ final row = CatalogItem( getComponent: itemContext.getComponent, explicitListBuilder: (childIds, buildChild, getComponent, dataContext) { return Row( - mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution), - crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment), + mainAxisAlignment: _parseMainAxisAlignment(rowData.justify), + crossAxisAlignment: _parseCrossAxisAlignment(rowData.align), mainAxisSize: MainAxisSize.min, - children: childIds - .map( - (componentId) => buildWeightedChild( - componentId: componentId, - dataContext: dataContext, - buildChild: buildChild, - weight: - getComponent(componentId)?.weight ?? - (getComponent(componentId)?.type == 'TextField' - ? 1 - : null), - ), - ) - .toList(), + children: childIds.map((componentId) { + final explicitWeight = + getComponent(componentId)?.properties['weight'] as int?; + final bool isImplicitlyFlexible = + itemContext + .getCatalogItem(getComponent(componentId)?.type ?? '') + ?.isImplicitlyFlexible ?? + false; + final int? weight = + explicitWeight ?? (isImplicitlyFlexible ? 1 : null); + final FlexFit fit = explicitWeight != null + ? FlexFit.tight + : FlexFit.loose; + + return buildWeightedChild( + componentId: componentId, + dataContext: dataContext, + buildChild: buildChild, + weight: weight, + flexFit: fit, + ); + }).toList(), ); }, - templateListWidgetBuilder: (context, list, componentId, dataBinding) { + templateListWidgetBuilder: (context, data, componentId, dataBinding) { + final List values; + final List keys; + + if (data is List) { + values = data; + keys = List.generate(data.length, (index) => index.toString()); + } else if (data is Map) { + values = data.values.toList(); + keys = data.keys.map((k) => k.toString()).toList(); + } else { + return const SizedBox.shrink(); + } + final Component? component = itemContext.getComponent(componentId); - final int? weight = - component?.weight ?? (component?.type == 'TextField' ? 1 : null); + final explicitWeight = component?.properties['weight'] as int?; + + final bool isImplicitlyFlexible = + itemContext + .getCatalogItem(component?.type ?? '') + ?.isImplicitlyFlexible ?? + false; + final int? weight = explicitWeight ?? (isImplicitlyFlexible ? 1 : null); + final FlexFit fit = explicitWeight != null + ? FlexFit.tight + : FlexFit.loose; return Row( - mainAxisAlignment: _parseMainAxisAlignment(rowData.distribution), - crossAxisAlignment: _parseCrossAxisAlignment(rowData.alignment), + mainAxisAlignment: _parseMainAxisAlignment(rowData.justify), + crossAxisAlignment: _parseCrossAxisAlignment(rowData.align), mainAxisSize: MainAxisSize.min, children: [ - for (var i = 0; i < list.length; i++) ...[ + for (var i = 0; i < values.length; i++) ...[ buildWeightedChild( componentId: componentId, dataContext: itemContext.dataContext.nested( - DataPath('$dataBinding/$i'), + '$dataBinding/${keys[i]}', ), buildChild: itemContext.buildChild, weight: weight, + flexFit: fit, + key: ValueKey(keys[i]), ), ], ], @@ -164,36 +190,21 @@ final row = CatalogItem( [ { "id": "root", - "component": { - "Row": { - "children": { - "explicitList": [ - "text1", - "text2" - ] - } - } - } + "component": "Row", + "children": [ + "text1", + "text2" + ] }, { "id": "text1", - "component": { - "Text": { - "text": { - "literalString": "First" - } - } - } + "component": "Text", + "text": "First" }, { "id": "text2", - "component": { - "Text": { - "text": { - "literalString": "Second" - } - } - } + "component": "Text", + "text": "Second" } ] ''', diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/slider.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/slider.dart new file mode 100644 index 000000000..aba4a90bb --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/slider.dart @@ -0,0 +1,182 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; +import 'widget_helpers.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['Slider']), + 'value': A2uiSchemas.numberReference(), + 'min': S.number(description: 'The minimum value. Defaults to 0.0.'), + 'max': S.number(description: 'The maximum value. Defaults to 1.0.'), + 'label': A2uiSchemas.stringReference( + description: 'The label for the slider.', + ), + 'checks': A2uiSchemas.checkable(), + }, + required: ['component', 'value'], +); + +extension type _SliderData.fromMap(JsonMap _json) { + factory _SliderData({ + required JsonMap value, + double? min, + double? max, + List? checks, + }) => _SliderData.fromMap({ + 'value': value, + 'min': min, + 'max': max, + 'checks': checks, + }); + + Object get value => _json['value'] as Object; + double get min => (_json['min'] as num?)?.toDouble() ?? 0.0; + double get max => (_json['max'] as num?)?.toDouble() ?? 1.0; + List? get checks => (_json['checks'] as List?)?.cast(); + + String? get label { + final Object? val = _json['label']; + if (val is String) return val; + if (val is Map && val.containsKey('value')) { + return val['value'] as String?; + } + return null; + } +} + +/// A catalog item representing a Material Design slider. +/// +/// This widget allows the user to select a value from a range by sliding a +/// thumb along a track. The `value` is bidirectionally bound to the data model. +/// This is analogous to Flutter's [Slider] widget. +/// +/// ## Parameters: +/// +/// - `value`: The current value of the slider. +/// - `min`: The minimum value of the slider. Defaults to 0.0. +/// - `max`: The maximum value of the slider. Defaults to 1.0. +/// - `label`: The label for the slider. +final slider = CatalogItem( + name: 'Slider', + dataSchema: _schema, + widgetBuilder: (CatalogItemContext itemContext) { + final sliderData = _SliderData.fromMap(itemContext.data as JsonMap); + final Object valueRef = sliderData.value; + final path = (valueRef is Map && valueRef.containsKey('path')) + ? valueRef['path'] as String + : '${itemContext.id}.value'; + + final ValueNotifier valueNotifier = itemContext.dataContext + .subscribe(path); + + final ValueNotifier labelNotifier = sliderData.label != null + ? itemContext.dataContext.subscribeToString(sliderData.label!) + : ValueNotifier(null); + + return ValueListenableBuilder( + valueListenable: valueNotifier, + builder: (context, value, child) { + // If value is null (nothing in DataContext yet), fall back to + // literal value if provided. + var effectiveValue = value; + if (effectiveValue == null) { + if (valueRef is num) { + effectiveValue = valueRef; + } + } + + final Widget sliderWidget = Padding( + padding: const EdgeInsetsDirectional.only(end: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Slider( + value: (effectiveValue ?? sliderData.min).toDouble(), + min: sliderData.min, + max: sliderData.max, + divisions: (sliderData.max - sliderData.min).toInt(), + onChanged: (newValue) { + itemContext.dataContext.update(path, newValue); + }, + ), + ), + Text( + value?.toStringAsFixed(0) ?? sliderData.min.toStringAsFixed(0), + ), + ], + ), + ); + + return ValueListenableBuilder( + valueListenable: itemContext.dataContext.createComputedNotifier( + checksToExpression(sliderData.checks), + ), + builder: (context, isValid, _) { + final isError = isValid == false; + + return ValueListenableBuilder( + valueListenable: labelNotifier, + builder: (context, label, child) { + final List children = []; + if (label != null && label.isNotEmpty) { + children.add( + Text(label, style: Theme.of(context).textTheme.bodySmall), + ); + } + children.add(sliderWidget); + if (isError) { + children.add( + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 4.0), + child: Text( + 'Invalid value', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ), + ); + } + + if (children.length == 1) { + return children.first; + } + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + }, + ); + }, + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Slider", + "min": 0, + "max": 10, + "value": { + "path": "/myValue" + } + } + ] + ''', + ], +); diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/tabs.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/tabs.dart new file mode 100644 index 000000000..f3e3920ba --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/tabs.dart @@ -0,0 +1,250 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; + +final _schema = S.object( + properties: { + 'component': S.string(enumValues: ['Tabs']), + 'tabs': S.list( + items: S.object( + properties: { + 'label': A2uiSchemas.stringReference( + description: 'The label for the tab.', + ), + 'content': A2uiSchemas.componentReference( + description: + 'The content (widget ID) to display when this tab is active.', + ), + }, + required: ['label', 'content'], + ), + ), + 'activeTab': A2uiSchemas.numberReference( + description: 'The index of the currently active tab.', + ), + }, + required: ['component', 'tabs'], +); + +extension type _TabsData.fromMap(JsonMap _json) { + factory _TabsData({required List tabs, Object? activeTab}) => + _TabsData.fromMap({'tabs': tabs, 'activeTab': activeTab}); + + List get tabs { + return (_json['tabs'] as List).cast(); + } + + Object? get activeTab => _json['activeTab']; +} + +class _TabsWidget extends StatefulWidget { + const _TabsWidget({ + required this.tabs, + required this.itemContext, + required this.activeTabNotifier, + this.initialTab = 0, + required this.onTabChanged, + }); + + final List tabs; + final CatalogItemContext itemContext; + final ValueNotifier activeTabNotifier; + final int initialTab; + final ValueChanged onTabChanged; + + @override + State<_TabsWidget> createState() => _TabsWidgetState(); +} + +class _TabsWidgetState extends State<_TabsWidget> + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + final int initialIndex = + (widget.activeTabNotifier.value?.toInt() ?? widget.initialTab).clamp( + 0, + widget.tabs.length - 1, + ); + _tabController = TabController( + length: widget.tabs.length, + vsync: this, + initialIndex: initialIndex, + ); + _tabController.addListener(_handleTabSelection); + widget.activeTabNotifier.addListener(_handleExternalChange); + } + + @override + void didUpdateWidget(_TabsWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.tabs.length != oldWidget.tabs.length) { + _tabController.dispose(); + final int initialIndex = + (widget.activeTabNotifier.value?.toInt() ?? widget.initialTab).clamp( + 0, + widget.tabs.length - 1, + ); + _tabController = TabController( + length: widget.tabs.length, + vsync: this, + initialIndex: initialIndex, + ); + _tabController.addListener(_handleTabSelection); + } + } + + void _handleTabSelection() { + if (!_tabController.indexIsChanging) { + widget.onTabChanged(_tabController.index); + } + } + + void _handleExternalChange() { + final int? newIndex = widget.activeTabNotifier.value?.toInt(); + if (newIndex != null && + newIndex >= 0 && + newIndex < widget.tabs.length && + newIndex != _tabController.index) { + _tabController.animateTo(newIndex); + } + } + + @override + void dispose() { + _tabController.dispose(); + widget.activeTabNotifier.removeListener(_handleExternalChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + TabBar( + controller: _tabController, + tabs: widget.tabs.map((tabItem) { + final Object? labelRef = tabItem['label'] ?? tabItem['title']; + final ValueNotifier titleNotifier = widget + .itemContext + .dataContext + .subscribeToString(labelRef); + return ValueListenableBuilder( + valueListenable: titleNotifier, + builder: (context, title, child) { + return Tab(text: title ?? ''); + }, + ); + }).toList(), + ), + SizedBox( + child: AnimatedBuilder( + animation: _tabController, + builder: (context, child) { + final int index = _tabController.index; + return IndexedStack( + index: index, + sizing: StackFit.loose, + children: widget.tabs.map((tabItem) { + final contentId = + (tabItem['content'] ?? tabItem['child']) as String; + return widget.itemContext.buildChild(contentId); + }).toList(), + ); + }, + ), + ), + ], + ); + } +} + +/// A catalog item representing a Material Design tab layout. +/// +/// This widget displays a [TabBar] and a view area to allow navigation +/// between different child components. Each tab in `tabs` has a label and +/// a corresponding child component ID to display when selected. +/// +/// ## Parameters: +/// +/// - `tabs`: A list of tabs to display, each with a `label` and a `content` +/// widget ID. +/// - `activeTab`: (Optional) Binding to the current tab index. +final tabs = CatalogItem( + name: 'Tabs', + dataSchema: _schema, + widgetBuilder: (itemContext) { + final tabsData = _TabsData.fromMap(itemContext.data as JsonMap); + final Object? activeTabRef = tabsData.activeTab; + final path = (activeTabRef is Map && activeTabRef.containsKey('path')) + ? activeTabRef['path'] as String + : '${itemContext.id}.activeTab'; + + final ValueNotifier activeTabNotifier = itemContext.dataContext + .subscribeToNumber({'path': path}); + + return ValueListenableBuilder( + valueListenable: activeTabNotifier, + builder: (context, currentActiveTab, child) { + var effectiveActiveTab = currentActiveTab; + if (effectiveActiveTab == null) { + if (activeTabRef is num) { + effectiveActiveTab = activeTabRef; + } + } + + return _TabsWidget( + tabs: tabsData.tabs, + itemContext: itemContext, + activeTabNotifier: activeTabNotifier, + initialTab: activeTabRef is num ? activeTabRef.toInt() : 0, + onTabChanged: (newIndex) { + itemContext.dataContext.update(path, newIndex); + }, + ); + }, + ); + }, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "Tabs", + "activeTab": { "path": "/currentTab" }, + "tabs": [ + { + "label": "Overview", + "content": "text1" + }, + { + "label": "Details", + "content": "text2" + } + ] + }, + { + "id": "text1", + "component": "Text", + "text": "This is a short summary of the item." + }, + { + "id": "text2", + "component": "Text", + "text": "This is a much longer, more detailed description." + } + ] + ''', + ], +); diff --git a/packages/genui/lib/src/catalog/core_widgets/text.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart similarity index 75% rename from packages/genui/lib/src/catalog/core_widgets/text.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart index 9bc32aedc..d8ccb61ef 100644 --- a/packages/genui/lib/src/catalog/core_widgets/text.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/text.dart @@ -6,17 +6,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; -import '../../core/widget_utilities.dart'; import '../../model/a2ui_schemas.dart'; import '../../model/catalog_item.dart'; import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; extension type _TextData.fromMap(JsonMap _json) { - factory _TextData({required JsonMap text, String? usageHint}) => - _TextData.fromMap({'text': text, 'usageHint': usageHint}); + factory _TextData({required Object text, String? variant}) => + _TextData.fromMap({'text': text, 'variant': variant}); - JsonMap get text => _json['text'] as JsonMap; - String? get usageHint => _json['usageHint'] as String?; + Object get text => _json['text'] as Object; + String? get variant => _json['variant'] as String?; } /// A catalog item representing a block of styled text. @@ -28,42 +28,39 @@ extension type _TextData.fromMap(JsonMap _json) { /// ## Parameters: /// /// - `text`: The text to display. This supports markdown. -/// - `usageHint`: A usage hint for the text size and style. One of 'h1', 'h2', +/// - `variant`: A hint for the text size and style. One of 'h1', 'h2', /// 'h3', 'h4', 'h5', 'caption', 'body'. final text = CatalogItem( name: 'Text', dataSchema: S.object( properties: { + 'component': S.string(enumValues: ['Text']), 'text': A2uiSchemas.stringReference( description: '''While simple Markdown is supported (without HTML or image references), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.''', ), - 'usageHint': S.string( - description: 'A usage hint for the base text style.', + 'variant': S.string( + description: 'A hint for the base text style.', enumValues: ['h1', 'h2', 'h3', 'h4', 'h5', 'caption', 'body'], ), }, - required: ['text'], + required: ['component', 'text'], ), exampleData: [ () => ''' [ { "id": "root", - "component": { - "Text": { - "text": { - "literalString": "Hello World" - }, - "usageHint": "h1" - } - } + "component": "Text", + "text": "Hello World", + "variant": "h1" } ] ''', ], widgetBuilder: (itemContext) { final textData = _TextData.fromMap(itemContext.data as JsonMap); + final ValueNotifier notifier = itemContext.dataContext .subscribeToString(textData.text); @@ -71,8 +68,8 @@ final text = CatalogItem( valueListenable: notifier, builder: (context, currentValue, child) { final TextTheme textTheme = Theme.of(context).textTheme; - final String usageHint = textData.usageHint ?? 'body'; - final TextStyle? baseStyle = switch (usageHint) { + final String variant = textData.variant ?? 'body'; + final TextStyle? baseStyle = switch (variant) { 'h1' => textTheme.headlineLarge, 'h2' => textTheme.headlineMedium, 'h3' => textTheme.headlineSmall, @@ -81,7 +78,7 @@ final text = CatalogItem( 'caption' => textTheme.bodySmall, _ => DefaultTextStyle.of(context).style, }; - final double verticalPadding = switch (usageHint) { + final double verticalPadding = switch (variant) { 'h1' => 20.0, 'h2' => 16.0, 'h3' => 12.0, diff --git a/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart new file mode 100644 index 000000000..5ce972a16 --- /dev/null +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/text_field.dart @@ -0,0 +1,292 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +import '../../functions/expression_parser.dart'; +import '../../model/a2ui_schemas.dart'; +import '../../model/catalog_item.dart'; +import '../../model/ui_models.dart'; +import '../../primitives/simple_items.dart'; +import '../../widgets/widget_utilities.dart'; + +final _schema = S.object( + description: 'A text input field.', + properties: { + 'component': S.string(enumValues: ['TextField']), + 'value': A2uiSchemas.stringReference( + description: 'The value of the text field.', + ), + 'label': A2uiSchemas.stringReference(), + 'variant': S.string( + enumValues: ['shortText', 'longText', 'number', 'obscured'], + ), + 'checks': A2uiSchemas.checkable(), + 'validationRegexp': S.string(), + 'onSubmittedAction': A2uiSchemas.action(), + }, +); + +extension type _TextFieldData.fromMap(JsonMap _json) { + factory _TextFieldData({ + Object? value, + Object? label, + List? checks, + String? variant, + String? validationRegexp, + JsonMap? onSubmittedAction, + }) => _TextFieldData.fromMap({ + 'value': value, + 'label': label, + 'checks': checks, + 'variant': variant, + 'validationRegexp': validationRegexp, + 'onSubmittedAction': onSubmittedAction, + }); + + Object? get value => _json['value']; + Object? get label => _json['label']; + List? get checks => (_json['checks'] as List?)?.cast(); + String? get variant => _json['variant'] as String?; + String? get validationRegexp => _json['validationRegexp'] as String?; + JsonMap? get onSubmittedAction => _json['onSubmittedAction'] as JsonMap?; +} + +class _TextField extends StatefulWidget { + const _TextField({ + required this.initialValue, + this.label, + this.checks, + this.parser, + this.textFieldType, + this.validationRegexp, + required this.onChanged, + required this.onSubmitted, + }); + + final String initialValue; + final String? label; + final List? checks; + final ExpressionParser? parser; + final String? textFieldType; + final String? validationRegexp; + final void Function(String) onChanged; + final void Function(String) onSubmitted; + + @override + State<_TextField> createState() => _TextFieldState(); +} + +class _TextFieldState extends State<_TextField> { + late final TextEditingController _controller; + String? _errorText; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialValue); + _errorText = _calculateError(widget.initialValue); + } + + @override + void didUpdateWidget(_TextField oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.initialValue != _controller.text) { + _controller.text = widget.initialValue; + final String? newError = _calculateError(widget.initialValue); + if (newError != _errorText) { + setState(() { + _errorText = newError; + }); + } + } else if (widget.checks != oldWidget.checks) { + // Re-validate if checks changed + final String? newError = _calculateError(_controller.text); + if (newError != _errorText) { + setState(() { + _errorText = newError; + }); + } + } + } + + String? _calculateError(String value) { + if (widget.checks == null || widget.parser == null) { + return null; + } + + for (final JsonMap check in widget.checks!) { + // Support both 'condition' wrapper (as seen in some samples) and direct + // logic expression + final Object? condition = check['condition']; + final bool isValid = widget.parser!.evaluateCondition(condition); + if (!isValid) { + return check['message'] as String? ?? 'Invalid value'; + } + } + return null; + } + + void _validate(String value) { + final String? newError = _calculateError(value); + if (newError != _errorText) { + setState(() => _errorText = newError); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: _controller, + decoration: InputDecoration( + labelText: widget.label, + errorText: _errorText, + ), + obscureText: widget.textFieldType == 'obscured', + keyboardType: switch (widget.textFieldType) { + 'number' => TextInputType.number, + 'longText' => TextInputType.multiline, + _ => TextInputType.text, + }, + onChanged: (val) { + widget.onChanged(val); + _validate(val); + }, + onSubmitted: (val) { + _validate(val); + if (_errorText == null) { + widget.onSubmitted(val); + } + }, + ); + } +} + +/// A catalog item representing a Material Design text field. +/// +/// This widget allows the user to enter and edit text. The `text` parameter +/// bidirectionally binds the field's content to the data model. This is +/// analogous to Flutter's [TextField] widget. +/// +/// ## Parameters: +/// +/// - `text`: The initial value of the text field. +/// - `label`: The text to display as the label for the text field. +/// - `textFieldType`: The type of text field. Can be `shortText`, `longText`, +/// `number`, `date`, or `obscured`. +/// - `validationRegexp`: A regular expression to validate the input. +/// - `onSubmittedAction`: The action to perform when the user submits the +/// text field. +final textField = CatalogItem( + name: 'TextField', + isImplicitlyFlexible: true, + dataSchema: _schema, + exampleData: [ + () => ''' + [ + { + "id": "root", + "component": "TextField", + "value": "Hello World", + "label": "Greeting" + } + ] + ''', + () => ''' + [ + { + "id": "root", + "component": "TextField", + "value": "password123", + "label": "Password", + "textFieldType": "obscured" + } + ] + ''', + ], + widgetBuilder: (itemContext) { + final textFieldData = _TextFieldData.fromMap(itemContext.data as JsonMap); + final Object? valueRef = textFieldData.value; + final path = (valueRef is Map && valueRef.containsKey('path')) + ? valueRef['path'] as String + : '${itemContext.id}.value'; + final ValueNotifier notifier = itemContext.dataContext + .subscribeToString({'path': path}); + final ValueNotifier labelNotifier = itemContext.dataContext + .subscribeToString(textFieldData.label); + + final parser = ExpressionParser(itemContext.dataContext); + + return ValueListenableBuilder( + valueListenable: notifier, + builder: (context, currentValue, child) { + return ValueListenableBuilder( + valueListenable: labelNotifier, + builder: (context, label, child) { + final String? effectiveValue = + currentValue?.toString() ?? + (valueRef is String ? valueRef : null); + + return _TextField( + initialValue: effectiveValue ?? '', + label: label, + checks: textFieldData.checks, + parser: parser, + textFieldType: textFieldData.variant, + validationRegexp: textFieldData.validationRegexp, + onChanged: (newValue) { + if (textFieldData.variant == 'number') { + final num? numberValue = num.tryParse(newValue); + if (numberValue != null) { + itemContext.dataContext.update(path, numberValue); + return; + } + } + itemContext.dataContext.update(path, newValue); + }, + onSubmitted: (newValue) { + final JsonMap? actionData = textFieldData.onSubmittedAction; + if (actionData == null) { + return; + } + + if (actionData.containsKey('event')) { + final eventMap = actionData['event'] as JsonMap; + final actionName = eventMap['name'] as String; + final contextDefinition = eventMap['context'] as JsonMap?; + final JsonMap resolvedContext = resolveContext( + itemContext.dataContext, + contextDefinition, + ); + itemContext.dispatchEvent( + UserActionEvent( + name: actionName, + sourceComponentId: itemContext.id, + context: resolvedContext, + ), + ); + } else if (actionData.containsKey('functionCall')) { + final funcMap = actionData['functionCall'] as JsonMap; + final callName = funcMap['call'] as String; + if (callName == 'closeModal') { + Navigator.of(itemContext.buildContext).pop(); + return; + } + parser.evaluateFunctionCall(funcMap); + } + }, + ); + }, + ); + }, + ); + }, +); diff --git a/packages/genui/lib/src/catalog/core_widgets/video.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/video.dart similarity index 84% rename from packages/genui/lib/src/catalog/core_widgets/video.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/video.dart index b75a4c5c8..69e27b11b 100644 --- a/packages/genui/lib/src/catalog/core_widgets/video.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/video.dart @@ -10,11 +10,12 @@ import '../../model/catalog_item.dart'; final _schema = S.object( properties: { + 'component': S.string(enumValues: ['Video']), 'url': A2uiSchemas.stringReference( description: 'The URL of the video to play.', ), }, - required: ['url'], + required: ['component', 'url'], ); /// A catalog item representing a video player. @@ -39,13 +40,8 @@ final video = CatalogItem( [ { "id": "root", - "component": { - "Video": { - "url": { - "literalString": "https://example.com/video.mp4" - } - } - } + "component": "Video", + "url": "https://example.com/video.mp4" } ] ''', diff --git a/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart b/packages/genui/lib/src/catalog/basic_catalog_widgets/widget_helpers.dart similarity index 72% rename from packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart rename to packages/genui/lib/src/catalog/basic_catalog_widgets/widget_helpers.dart index 80052745b..f322f082e 100644 --- a/packages/genui/lib/src/catalog/core_widgets/widget_helpers.dart +++ b/packages/genui/lib/src/catalog/basic_catalog_widgets/widget_helpers.dart @@ -16,9 +16,9 @@ import '../../primitives/simple_items.dart'; typedef TemplateListWidgetBuilder = Widget Function( BuildContext context, - Map data, + Object? data, String componentId, - String dataBinding, + String path, ); /// Builder function for creating a parent widget given a list of pre-built @@ -41,10 +41,8 @@ typedef ExplicitListWidgetBuilder = /// 1. An explicit list of child widget IDs. /// 2. A template with a data binding to a list of data. /// -/// The `childrenData` can be a `List` of child IDs, or a `JsonMap` -/// with either an `explicitList` key (with a `List` value) or a -/// `template` key. The `template` is a `JsonMap` with `dataBinding` and -/// `componentId` keys. +/// The `childrenData` can be a `List` of child IDs, or a [JsonMap] +/// defining a template structure with `path` and `componentId` keys. class ComponentChildrenBuilder extends StatelessWidget { /// Creates a new [ComponentChildrenBuilder]. const ComponentChildrenBuilder({ @@ -77,12 +75,12 @@ class ComponentChildrenBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - final List? explicitList = (childrenData is List) - ? (childrenData as List).cast() - : ((childrenData as JsonMap?)?['explicitList'] as List?) - ?.cast(); + if (childrenData is List) { + final List explicitList = (childrenData as List).map((e) { + if (e is String) return e; + return e.toString(); + }).toList(); - if (explicitList != null) { return explicitListBuilder( explicitList, buildChild, @@ -93,28 +91,24 @@ class ComponentChildrenBuilder extends StatelessWidget { if (childrenData is JsonMap) { final childrenMap = childrenData as JsonMap; - final template = childrenMap['template'] as JsonMap?; - if (template != null) { - final dataBinding = template['dataBinding'] as String; - final componentId = template['componentId'] as String; + if (childrenMap.containsKey('path') && + childrenMap.containsKey('componentId')) { + final path = childrenMap['path'] as String; + final componentId = childrenMap['componentId'] as String; genUiLogger.finest( 'Widget $componentId subscribing to ${dataContext.path}', ); - final ValueNotifier?> dataNotifier = dataContext - .subscribe>(DataPath(dataBinding)); - return ValueListenableBuilder?>( + final ValueNotifier dataNotifier = dataContext + .subscribe(path); + return ValueListenableBuilder( valueListenable: dataNotifier, builder: (context, data, child) { - genUiLogger.info( - 'ComponentChildrenBuilder: data type: ${data.runtimeType}, ' - 'value: $data', - ); if (data != null) { return templateListWidgetBuilder( context, data, componentId, - dataBinding, + path, ); } return const SizedBox.shrink(); @@ -133,10 +127,31 @@ Widget buildWeightedChild({ required DataContext dataContext, required ChildBuilderCallback buildChild, required int? weight, + Key? key, + FlexFit flexFit = FlexFit.loose, }) { final Widget childWidget = buildChild(componentId, dataContext); if (weight != null) { - return Flexible(flex: weight, child: childWidget); + return Flexible(key: key, flex: weight, fit: flexFit, child: childWidget); + } + if (key != null) { + return KeyedSubtree(key: key, child: childWidget); } return childWidget; } + +/// Converts a list of validation checks into a single expression that evaluates +/// to true if all checks pass. +Object? checksToExpression(List? checks) { + if (checks == null || checks.isEmpty) { + return true; + } + + // Combine all checks into a single 'and' condition + return { + 'functionCall': { + 'call': 'and', + 'args': {'values': checks.map((c) => c['condition']).toList()}, + }, + }; +} diff --git a/packages/genui/lib/src/catalog/core_widgets/audio_player.dart b/packages/genui/lib/src/catalog/core_widgets/audio_player.dart deleted file mode 100644 index 9d58af8ae..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/audio_player.dart +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; - -final _schema = S.object( - properties: { - 'url': A2uiSchemas.stringReference( - description: 'The URL of the audio to play.', - ), - }, - required: ['url'], -); - -/// A catalog item for an audio player. -/// -/// This widget displays a placeholder for an audio player, used to represent -/// a component capable of playing audio from a given URL. -/// -/// ## Parameters: -/// -/// - `url`: The URL of the audio to play. -final audioPlayer = CatalogItem( - name: 'AudioPlayer', - dataSchema: _schema, - widgetBuilder: (itemContext) { - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200, maxHeight: 100), - child: const Placeholder(child: Center(child: Text('AudioPlayer'))), - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "AudioPlayer": { - "url": { - "literalString": "https://example.com/audio.mp3" - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/button.dart b/packages/genui/lib/src/catalog/core_widgets/button.dart deleted file mode 100644 index 6de2f2857..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/button.dart +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: avoid_dynamic_calls - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/ui_models.dart'; -import '../../primitives/logging.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'child': A2uiSchemas.componentReference( - description: - 'The ID of a child widget. This should always be set, e.g. to the ID ' - 'of a `Text` widget.', - ), - 'action': A2uiSchemas.action(), - 'primary': S.boolean( - description: 'Whether the button invokes a primary action.', - ), - }, - required: ['child', 'action'], -); - -extension type _ButtonData.fromMap(JsonMap _json) { - factory _ButtonData({ - required String child, - required JsonMap action, - bool primary = false, - }) => _ButtonData.fromMap({ - 'child': child, - 'action': action, - 'primary': primary, - }); - - String get child => _json['child'] as String; - JsonMap get action => _json['action'] as JsonMap; - bool get primary => (_json['primary'] as bool?) ?? false; -} - -/// A catalog item representing a Material Design elevated button. -/// -/// This widget displays an interactive button. When pressed, it dispatches -/// the specified `action` event. The button's appearance can be styled as -/// a primary action. -/// -/// ## Parameters: -/// -/// - `child`: The ID of a child widget to display inside the button. -/// - `action`: The action to perform when the button is pressed. -/// - `primary`: Whether the button invokes a primary action (defaults to -/// false). -final button = CatalogItem( - name: 'Button', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final buttonData = _ButtonData.fromMap(itemContext.data as JsonMap); - final Widget child = itemContext.buildChild(buttonData.child); - final JsonMap actionData = buttonData.action; - final actionName = actionData['name'] as String; - final List contextDefinition = - (actionData['context'] as List?) ?? []; - - genUiLogger.info('Building Button with child: ${buttonData.child}'); - final ColorScheme colorScheme = Theme.of( - itemContext.buildContext, - ).colorScheme; - final bool primary = buttonData.primary; - - final TextStyle? textStyle = Theme.of(itemContext.buildContext) - .textTheme - .bodyLarge - ?.copyWith( - color: primary ? colorScheme.onPrimary : colorScheme.onSurface, - ); - - return ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: primary ? colorScheme.primary : colorScheme.surface, - foregroundColor: primary - ? colorScheme.onPrimary - : colorScheme.onSurface, - ).copyWith(textStyle: WidgetStatePropertyAll(textStyle)), - onPressed: () { - final JsonMap resolvedContext = resolveContext( - itemContext.dataContext, - contextDefinition, - ); - itemContext.dispatchEvent( - UserActionEvent( - name: actionName, - sourceComponentId: itemContext.id, - context: resolvedContext, - ), - ); - }, - child: child, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Button": { - "child": "text", - "action": { - "name": "button_pressed" - } - } - } - }, - { - "id": "text", - "component": { - "Text": { - "text": { - "literalString": "Hello World" - } - } - } - } - ] - ''', - () => ''' - [ - { - "id": "root", - "component": { - "Column": { - "children": { - "explicitList": ["primaryButton", "secondaryButton"] - } - } - } - }, - { - "id": "primaryButton", - "component": { - "Button": { - "child": "primaryText", - "primary": true, - "action": { - "name": "primary_pressed" - } - } - } - }, - { - "id": "secondaryButton", - "component": { - "Button": { - "child": "secondaryText", - "action": { - "name": "secondary_pressed" - } - } - } - }, - { - "id": "primaryText", - "component": { - "Text": { - "text": { - "literalString": "Primary Button" - } - } - } - }, - { - "id": "secondaryText", - "component": { - "Text": { - "text": { - "literalString": "Secondary Button" - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/check_box.dart b/packages/genui/lib/src/catalog/core_widgets/check_box.dart deleted file mode 100644 index 17e896e90..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/check_box.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'label': A2uiSchemas.stringReference(), - 'value': A2uiSchemas.booleanReference(), - }, - required: ['label', 'value'], -); - -extension type _CheckBoxData.fromMap(JsonMap _json) { - factory _CheckBoxData({required JsonMap label, required JsonMap value}) => - _CheckBoxData.fromMap({'label': label, 'value': value}); - - JsonMap get label => _json['label'] as JsonMap; - JsonMap get value => _json['value'] as JsonMap; -} - -/// A catalog item representing a Material Design checkbox with a label. -/// -/// This widget displays a checkbox a [Text] label. The checkbox's state -/// is bidirectionally bound to the data model path specified in the `value` -/// parameter. -/// -/// ## Parameters: -/// -/// - `label`: The text to display next to the checkbox. -/// - `value`: The boolean value of the checkbox. -final checkBox = CatalogItem( - name: 'CheckBox', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final checkBoxData = _CheckBoxData.fromMap(itemContext.data as JsonMap); - final ValueNotifier labelNotifier = itemContext.dataContext - .subscribeToString(checkBoxData.label); - final ValueNotifier valueNotifier = itemContext.dataContext - .subscribeToBool(checkBoxData.value); - return ValueListenableBuilder( - valueListenable: labelNotifier, - builder: (context, label, child) { - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, child) { - return CheckboxListTile( - controlAffinity: ListTileControlAffinity.leading, - title: Text( - label ?? '', - style: Theme.of(context).textTheme.bodyMedium, - ), - value: value ?? false, - onChanged: (newValue) { - final path = checkBoxData.value['path'] as String?; - if (path != null) { - itemContext.dataContext.update(DataPath(path), newValue); - } - }, - ); - }, - ); - }, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "CheckBox": { - "label": { - "literalString": "Check me" - }, - "value": { - "path": "/myValue", - "literalBoolean": true - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/date_time_input.dart b/packages/genui/lib/src/catalog/core_widgets/date_time_input.dart deleted file mode 100644 index 7cb7c838d..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/date_time_input.dart +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'value': A2uiSchemas.stringReference( - description: 'The selected date and/or time.', - ), - 'enableDate': S.boolean(), - 'enableTime': S.boolean(), - 'firstDate': S.string( - description: - 'The earliest selectable date (YYYY-MM-DD). Defaults to -9999-01-01.', - ), - 'lastDate': S.string( - description: - 'The latest selectable date (YYYY-MM-DD). Defaults to 9999-12-31.', - ), - }, - required: ['value'], -); - -extension type _DateTimeInputData.fromMap(JsonMap _json) { - factory _DateTimeInputData({ - required JsonMap value, - bool? enableDate, - bool? enableTime, - String? firstDate, - String? lastDate, - }) => _DateTimeInputData.fromMap({ - 'value': value, - 'enableDate': enableDate, - 'enableTime': enableTime, - 'firstDate': firstDate, - 'lastDate': lastDate, - }); - - JsonMap get value => _json['value'] as JsonMap; - bool get enableDate => (_json['enableDate'] as bool?) ?? true; - bool get enableTime => (_json['enableTime'] as bool?) ?? true; - DateTime get firstDate => - DateTime.tryParse(_json['firstDate'] as String? ?? '') ?? DateTime(-9999); - DateTime get lastDate => - DateTime.tryParse(_json['lastDate'] as String? ?? '') ?? - DateTime(9999, 12, 31); -} - -/// A catalog item representing a Material Design date and/or time input field. -/// -/// This widget displays a field that, when tapped, opens the native date and/or -/// time pickers. The selected value is stored as a string in the data model -/// path specified by the `value` parameter. -/// -/// ## Parameters: -/// -/// - `value`: The selected date and/or time, as a string. -/// - `enableDate`: Whether to allow the user to select a date. Defaults to -/// `true`. -/// - `enableTime`: Whether to allow the user to select a time. Defaults to -/// `true`. -/// - `outputFormat`: The format to use for the output string. -final dateTimeInput = CatalogItem( - name: 'DateTimeInput', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final dateTimeInputData = _DateTimeInputData.fromMap( - itemContext.data as JsonMap, - ); - final ValueNotifier valueNotifier = itemContext.dataContext - .subscribeToString(dateTimeInputData.value); - - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, child) { - final MaterialLocalizations localizations = MaterialLocalizations.of( - context, - ); - final String displayText = _getDisplayText( - value, - dateTimeInputData, - localizations, - ); - - return ListTile( - key: Key(itemContext.id), - title: Text(displayText, key: Key('${itemContext.id}_text')), - onTap: () => _handleTap( - context: itemContext.buildContext, - dataContext: itemContext.dataContext, - data: dateTimeInputData, - value: value, - ), - ); - }, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "DateTimeInput": { - "value": { - "path": "/myDateTime" - } - } - } - } - ] - ''', - () => ''' - [ - { - "id": "root", - "component": { - "DateTimeInput": { - "value": { - "path": "/myDate" - }, - "enableTime": false - } - } - } - ] - ''', - () => ''' - [ - { - "id": "root", - "component": { - "DateTimeInput": { - "value": { - "path": "/myTime" - }, - "enableDate": false - } - } - } - ] - ''', - ], -); - -Future _handleTap({ - required BuildContext context, - required DataContext dataContext, - required _DateTimeInputData data, - required String? value, -}) async { - final path = data.value['path'] as String?; - if (path == null) { - return; - } - - final DateTime initialDate = - DateTime.tryParse(value ?? '') ?? - DateTime.tryParse('1970-01-01T$value') ?? - DateTime.now(); - - var resultDate = initialDate; - var resultTime = TimeOfDay.fromDateTime(initialDate); - - if (data.enableDate) { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: initialDate, - firstDate: data.firstDate, - lastDate: data.lastDate, - ); - if (pickedDate == null) return; // User cancelled. - resultDate = pickedDate; - } - - if (data.enableTime) { - final TimeOfDay? pickedTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime(initialDate), - ); - if (pickedTime == null) return; // User cancelled. - resultTime = pickedTime; - } - - final finalDateTime = DateTime( - resultDate.year, - resultDate.month, - resultDate.day, - data.enableTime ? resultTime.hour : 0, - data.enableTime ? resultTime.minute : 0, - ); - - String formattedValue; - - if (data.enableDate && !data.enableTime) { - formattedValue = finalDateTime.toIso8601String().split('T').first; - } else if (!data.enableDate && data.enableTime) { - final String hour = finalDateTime.hour.toString().padLeft(2, '0'); - final String minute = finalDateTime.minute.toString().padLeft(2, '0'); - formattedValue = '$hour:$minute:00'; - } else { - // Both enabled (or both disabled, which shouldn't happen), - // write full ISO string. - formattedValue = finalDateTime.toIso8601String(); - } - - dataContext.update(DataPath(path), formattedValue); -} - -String _getDisplayText( - String? value, - _DateTimeInputData data, - MaterialLocalizations localizations, -) { - String getPlaceholderText() { - if (data.enableDate && data.enableTime) { - return 'Select a date and time'; - } else if (data.enableDate) { - return 'Select a date'; - } else if (data.enableTime) { - return 'Select a time'; - } - return 'Select a date/time'; - } - - DateTime? tryParseDateOrTime(String value) { - return DateTime.tryParse(value) ?? DateTime.tryParse('1970-01-01T$value'); - } - - String formatDateTime(DateTime date) { - final List parts = [ - if (data.enableDate) localizations.formatFullDate(date), - if (data.enableTime) - localizations.formatTimeOfDay(TimeOfDay.fromDateTime(date)), - ]; - return parts.join(' '); - } - - if (value == null) { - return getPlaceholderText(); - } - - final DateTime? date = tryParseDateOrTime(value); - if (date == null) { - return value; - } - - return formatDateTime(date); -} diff --git a/packages/genui/lib/src/catalog/core_widgets/image.dart b/packages/genui/lib/src/catalog/core_widgets/image.dart deleted file mode 100644 index 079814bbd..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/image.dart +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../primitives/logging.dart'; -import '../../primitives/simple_items.dart'; - -Schema _schema({required bool enableUsageHint}) { - final Map properties = { - 'url': A2uiSchemas.stringReference( - description: - 'Asset path (e.g. assets/...) or network URL (e.g. https://...)', - ), - 'fit': S.string( - description: 'How the image should be inscribed into the box.', - enumValues: BoxFit.values.map((e) => e.name).toList(), - ), - }; - if (enableUsageHint) { - properties['usageHint'] = S.string( - description: '''A hint for the image size and style. One of: - - icon: Small square icon. - - avatar: Circular avatar image. - - smallFeature: Small feature image. - - mediumFeature: Medium feature image. - - largeFeature: Large feature image. - - header: Full-width, full bleed, header image.''', - enumValues: [ - 'icon', - 'avatar', - 'smallFeature', - 'mediumFeature', - 'largeFeature', - 'header', - ], - ); - } - return S.object(properties: properties); -} - -extension type _ImageData.fromMap(JsonMap _json) { - factory _ImageData({required JsonMap url, String? fit, String? usageHint}) => - _ImageData.fromMap({'url': url, 'fit': fit, 'usageHint': usageHint}); - - JsonMap get url => _json['url'] as JsonMap; - BoxFit? get fit => _json['fit'] != null - ? BoxFit.values.firstWhere((e) => e.name == _json['fit'] as String) - : null; - String? get usageHint => _json['usageHint'] as String?; -} - -/// Returns a catalog item representing a widget that displays an image. -CatalogItem _imageCatalogItem({ - /// When set to `true`, the `usageHint` parameter will be included in the - /// schema for the image widget. This allows the AI model to provide hints - /// for the image's size and style, such as 'icon', 'avatar', or 'header'. - /// When set to `false`, the `usageHint` parameter is omitted from the schema, - /// preventing the model from using it. - required bool enableUsageHint, -}) { - return CatalogItem( - name: 'Image', - dataSchema: _schema(enableUsageHint: enableUsageHint), - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Image": { - "url": { - "literalString": "https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png" - }, - "usageHint": "mediumFeature" - } - } - } - ] - ''', - ], - widgetBuilder: (itemContext) { - final imageData = _ImageData.fromMap(itemContext.data as JsonMap); - final ValueNotifier notifier = itemContext.dataContext - .subscribeToString(imageData.url); - - return ValueListenableBuilder( - valueListenable: notifier, - builder: (context, currentLocation, child) { - final location = currentLocation; - if (location == null || location.isEmpty) { - genUiLogger.warning( - 'Image widget created with no URL at path: ' - '${itemContext.dataContext.path}', - ); - return const SizedBox.shrink(); - } - final BoxFit? fit = imageData.fit; - final String? usageHint = imageData.usageHint; - - late Widget child; - - if (location.startsWith('assets/')) { - child = Image.asset(location, fit: fit); - } else { - child = Image.network(location, fit: fit); - } - - if (usageHint == 'avatar') { - child = CircleAvatar(child: child); - } - - if (usageHint == 'header') { - return SizedBox(width: double.infinity, child: child); - } - - final double size = switch (usageHint) { - 'icon' || 'avatar' => 32.0, - 'smallFeature' => 50.0, - 'mediumFeature' => 150.0, - 'largeFeature' => 400.0, - _ => 150.0, - }; - - return SizedBox(width: size, height: size, child: child); - }, - ); - }, - ); -} - -/// A catalog item representing a widget that displays an image. -/// -/// The image source is specified by the `url` parameter, which can be a network -/// URL (e.g., `https://...`) or a local asset path (e.g., `assets/...`). -/// -/// ## Parameters: -/// -/// - `url`: The URL of the image to display. Can be a network URL or a local -/// asset path. -/// - `fit`: How the image should be inscribed into the box. See [BoxFit] for -/// possible values. -/// - `usageHint`: A usage hint for the image size and style. One of 'icon', -/// 'avatar', 'smallFeature', 'mediumFeature', 'largeFeature', 'header'. -final CatalogItem image = _imageCatalogItem(enableUsageHint: true); - -/// A variant of the image catalog item which does not expose a usageHint to let -/// the LLM determine the size. Instead, it is always medium sized. -/// -/// See [image] for full documentation. -final CatalogItem imageFixedSize = _imageCatalogItem(enableUsageHint: false); diff --git a/packages/genui/lib/src/catalog/core_widgets/list.dart b/packages/genui/lib/src/catalog/core_widgets/list.dart deleted file mode 100644 index 42e9c08e8..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/list.dart +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../primitives/simple_items.dart'; -import 'widget_helpers.dart'; - -final _schema = S.object( - properties: { - 'children': A2uiSchemas.componentArrayReference(), - 'direction': S.string(enumValues: ['vertical', 'horizontal']), - 'alignment': S.string(enumValues: ['start', 'center', 'end', 'stretch']), - }, - required: ['children'], -); - -extension type _ListData.fromMap(JsonMap _json) { - factory _ListData({ - required Object? children, - String? direction, - String? alignment, - }) => _ListData.fromMap({ - 'children': children, - 'direction': direction, - 'alignment': alignment, - }); - - Object? get children => _json['children']; - String? get direction => _json['direction'] as String?; - String? get alignment => _json['alignment'] as String?; -} - -/// A catalog item representing a scrollable list of widgets. -/// -/// This widget is analogous to Flutter's [ListView] widget. It can display -/// children in either a vertical or horizontal direction. -/// -/// ## Parameters: -/// -/// - `children`: A list of child widget IDs to display in the list. -/// - `direction`: The direction of the list. Can be `vertical` or -/// `horizontal`. Defaults to `vertical`. -/// - `alignment`: How the children should be placed along the cross axis. -/// Can be `start`, `center`, `end`, or `stretch`. Defaults to `start`. -final list = CatalogItem( - name: 'List', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final listData = _ListData.fromMap(itemContext.data as JsonMap); - final Axis direction = listData.direction == 'horizontal' - ? Axis.horizontal - : Axis.vertical; - return ComponentChildrenBuilder( - childrenData: listData.children, - dataContext: itemContext.dataContext, - buildChild: itemContext.buildChild, - getComponent: itemContext.getComponent, - explicitListBuilder: (childIds, buildChild, getComponent, dataContext) { - return ListView( - shrinkWrap: true, - scrollDirection: direction, - children: childIds.map((id) => buildChild(id, dataContext)).toList(), - ); - }, - templateListWidgetBuilder: - (context, Map data, componentId, dataBinding) { - final List values = data.values.toList(); - final List keys = data.keys.toList(); - return ListView.builder( - shrinkWrap: true, - scrollDirection: direction, - itemCount: values.length, - itemBuilder: (context, index) { - final DataContext itemDataContext = itemContext.dataContext - .nested(DataPath('$dataBinding/${keys[index]}')); - return itemContext.buildChild(componentId, itemDataContext); - }, - ); - }, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "List": { - "children": { - "explicitList": [ - "text1", - "text2" - ] - } - } - } - }, - { - "id": "text1", - "component": { - "Text": { - "text": { - "literalString": "First" - } - } - } - }, - { - "id": "text2", - "component": { - "Text": { - "text": { - "literalString": "Second" - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/modal.dart b/packages/genui/lib/src/catalog/core_widgets/modal.dart deleted file mode 100644 index a0023a75c..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/modal.dart +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// @docImport '../../core/genui_surface.dart'; -library; - -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'entryPointChild': A2uiSchemas.componentReference( - description: 'The widget that opens the modal.', - ), - 'contentChild': A2uiSchemas.componentReference( - description: 'The widget to display in the modal.', - ), - }, - required: ['entryPointChild', 'contentChild'], -); - -extension type _ModalData.fromMap(JsonMap _json) { - factory _ModalData({ - required String entryPointChild, - required String contentChild, - }) => _ModalData.fromMap({ - 'entryPointChild': entryPointChild, - 'contentChild': contentChild, - }); - - String get entryPointChild => _json['entryPointChild'] as String; - String get contentChild => _json['contentChild'] as String; -} - -/// A catalog item representing a modal bottom sheet. -/// -/// This component doesn't render the modal content directly. Instead, it -/// renders the `entryPointChild` widget. The `entryPointChild` is expected to -/// trigger an action (e.g., on button press) that causes the `contentChild` to -/// be displayed within a modal bottom sheet by the [GenUiSurface]. -/// -/// ## Parameters: -/// -/// - `entryPointChild`: The ID of the widget that opens the modal. -/// - `contentChild`: The ID of the widget to display in the modal. -final modal = CatalogItem( - name: 'Modal', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final modalData = _ModalData.fromMap(itemContext.data as JsonMap); - return itemContext.buildChild(modalData.entryPointChild); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Modal": { - "entryPointChild": "button", - "contentChild": "text" - } - } - }, - { - "id": "button", - "component": { - "Button": { - "child": "button_text", - "action": { - "name": "showModal", - "context": [ - { - "key": "modalId", - "value": { - "literalString": "root" - } - } - ] - } - } - } - }, - { - "id": "button_text", - "component": { - "Text": { - "text": { - "literalString": "Open Modal" - } - } - } - }, - { - "id": "text", - "component": { - "Text": { - "text": { - "literalString": "This is a modal." - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/multiple_choice.dart b/packages/genui/lib/src/catalog/core_widgets/multiple_choice.dart deleted file mode 100644 index 8a6fba7e8..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/multiple_choice.dart +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'selections': A2uiSchemas.stringArrayReference(), - 'options': S.list( - items: S.object( - properties: { - 'label': A2uiSchemas.stringReference(), - 'value': S.string(), - }, - required: ['label', 'value'], - ), - ), - 'maxAllowedSelections': S.integer(), - }, - required: ['selections', 'options'], -); - -extension type _MultipleChoiceData.fromMap(JsonMap _json) { - factory _MultipleChoiceData({ - required JsonMap selections, - required List options, - int? maxAllowedSelections, - }) => _MultipleChoiceData.fromMap({ - 'selections': selections, - 'options': options, - 'maxAllowedSelections': maxAllowedSelections, - }); - - JsonMap get selections => _json['selections'] as JsonMap; - List get options => (_json['options'] as List).cast(); - int? get maxAllowedSelections => - (_json['maxAllowedSelections'] as num?)?.toInt(); -} - -/// A catalog item representing a multiple choice selection widget. -/// -/// This widget displays a list of options, each with a checkbox. The -/// `selections` parameter, which should be a data model path, is updated to -/// reflect the list of *values* of the currently selected options. -/// -/// ## Parameters: -/// -/// - `selections`: A list of the values of the selected options. -/// - `options`: A list of options to display, each with a `label` and a -/// `value`. -/// - `maxAllowedSelections`: The maximum number of options that can be -/// selected. -final multipleChoice = CatalogItem( - name: 'MultipleChoice', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final multipleChoiceData = _MultipleChoiceData.fromMap( - itemContext.data as JsonMap, - ); - final ValueNotifier?> selectionsNotifier = itemContext - .dataContext - .subscribeToObjectArray(multipleChoiceData.selections); - - return ValueListenableBuilder?>( - valueListenable: selectionsNotifier, - builder: (context, selections, child) { - return Column( - children: multipleChoiceData.options.map((option) { - final ValueNotifier labelNotifier = itemContext.dataContext - .subscribeToString(option['label'] as JsonMap); - final value = option['value'] as String; - return ValueListenableBuilder( - valueListenable: labelNotifier, - builder: (context, label, child) { - if (multipleChoiceData.maxAllowedSelections == 1) { - final Object? groupValue = selections?.isNotEmpty == true - ? selections!.first - : null; - return RadioListTile( - controlAffinity: ListTileControlAffinity.leading, - dense: true, - title: Text( - label ?? '', - style: Theme.of(context).textTheme.bodyMedium, - ), - value: value, - // ignore: deprecated_member_use - groupValue: groupValue is String ? groupValue : null, - // ignore: deprecated_member_use - onChanged: (newValue) { - final path = - multipleChoiceData.selections['path'] as String?; - if (path == null || newValue == null) { - return; - } - itemContext.dataContext.update(DataPath(path), [ - newValue, - ]); - }, - ); - } else { - return CheckboxListTile( - title: Text(label ?? ''), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - value: selections?.contains(value) ?? false, - onChanged: (newValue) { - final path = - multipleChoiceData.selections['path'] as String?; - if (path == null) { - return; - } - final List newSelections = - selections?.map((e) => e.toString()).toList() ?? - []; - if (newValue ?? false) { - if (multipleChoiceData.maxAllowedSelections == null || - newSelections.length < - multipleChoiceData.maxAllowedSelections!) { - newSelections.add(value); - } - } else { - newSelections.remove(value); - } - itemContext.dataContext.update( - DataPath(path), - newSelections, - ); - }, - ); - } - }, - ); - }).toList(), - ); - }, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Column": { - "children": { - "explicitList": [ - "heading1", - "singleChoice", - "heading2", - "multiChoice" - ] - } - } - } - }, - { - "id": "heading1", - "component": { - "Text": { - "text": { - "literalString": "Single Selection (maxAllowedSelections: 1)" - } - } - } - }, - { - "id": "singleChoice", - "component": { - "MultipleChoice": { - "selections": { - "path": "/singleSelection" - }, - "maxAllowedSelections": 1, - "options": [ - { - "label": { - "literalString": "Option A" - }, - "value": "A" - }, - { - "label": { - "literalString": "Option B" - }, - "value": "B" - } - ] - } - } - }, - { - "id": "heading2", - "component": { - "Text": { - "text": { - "literalString": "Multiple Selections (unlimited)" - } - } - } - }, - { - "id": "multiChoice", - "component": { - "MultipleChoice": { - "selections": { - "path": "/multiSelection" - }, - "options": [ - { - "label": { - "literalString": "Option X" - }, - "value": "X" - }, - { - "label": { - "literalString": "Option Y" - }, - "value": "Y" - }, - { - "label": { - "literalString": "Option Z" - }, - "value": "Z" - } - ] - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/slider.dart b/packages/genui/lib/src/catalog/core_widgets/slider.dart deleted file mode 100644 index c4a1f5c2a..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/slider.dart +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'value': A2uiSchemas.numberReference(), - 'minValue': S.number(), - 'maxValue': S.number(), - }, - required: ['value'], -); - -extension type _SliderData.fromMap(JsonMap _json) { - factory _SliderData({ - required JsonMap value, - double? minValue, - double? maxValue, - }) => _SliderData.fromMap({ - 'value': value, - 'minValue': minValue, - 'maxValue': maxValue, - }); - - JsonMap get value => _json['value'] as JsonMap; - double get minValue => (_json['minValue'] as num?)?.toDouble() ?? 0.0; - double get maxValue => (_json['maxValue'] as num?)?.toDouble() ?? 1.0; -} - -/// A catalog item representing a Material Design slider. -/// -/// This widget allows the user to select a value from a range by sliding a -/// thumb along a track. The `value` is bidirectionally bound to the data model. -/// This is analogous to Flutter's [Slider] widget. -/// -/// ## Parameters: -/// -/// - `value`: The current value of the slider. -/// - `minValue`: The minimum value of the slider. Defaults to 0.0. -/// - `maxValue`: The maximum value of the slider. Defaults to 1.0. -final slider = CatalogItem( - name: 'Slider', - dataSchema: _schema, - widgetBuilder: (CatalogItemContext itemContext) { - final sliderData = _SliderData.fromMap(itemContext.data as JsonMap); - final ValueNotifier valueNotifier = itemContext.dataContext - .subscribeToValue(sliderData.value, 'literalNumber'); - - return ValueListenableBuilder( - valueListenable: valueNotifier, - builder: (context, value, child) { - return Padding( - padding: const EdgeInsetsDirectional.only(end: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Slider( - value: (value ?? sliderData.minValue).toDouble(), - min: sliderData.minValue, - max: sliderData.maxValue, - divisions: (sliderData.maxValue - sliderData.minValue) - .toInt(), - onChanged: (newValue) { - final path = sliderData.value['path'] as String?; - if (path != null) { - itemContext.dataContext.update(DataPath(path), newValue); - } - }, - ), - ), - Text( - value?.toStringAsFixed(0) ?? - sliderData.minValue.toStringAsFixed(0), - ), - ], - ), - ); - }, - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Slider": { - "minValue": 0, - "maxValue": 10, - "value": { - "path": "/myValue", - "literalNumber": 5 - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/tabs.dart b/packages/genui/lib/src/catalog/core_widgets/tabs.dart deleted file mode 100644 index 57294d40d..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/tabs.dart +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - properties: { - 'tabItems': S.list( - items: S.object( - properties: { - 'title': A2uiSchemas.stringReference(), - 'child': A2uiSchemas.componentReference(), - }, - required: ['title', 'child'], - ), - ), - }, - required: ['tabItems'], -); - -extension type _TabsData.fromMap(JsonMap _json) { - factory _TabsData({required List tabItems}) => - _TabsData.fromMap({'tabItems': tabItems}); - - List get tabItems => (_json['tabItems'] as List).cast(); -} - -/// A catalog item representing a Material Design tab layout. -/// -/// This widget displays a [TabBar] and a [TabBarView] to allow navigation -/// between different child components. Each tab in `tabItems` has a title and -/// a corresponding child component ID to display when selected. -/// -/// ## Parameters: -/// -/// - `tabItems`: A list of tabs to display, each with a `title` and a `child` -/// widget ID. -final tabs = CatalogItem( - name: 'Tabs', - dataSchema: _schema, - widgetBuilder: (itemContext) { - final tabsData = _TabsData.fromMap(itemContext.data as JsonMap); - return DefaultTabController( - length: tabsData.tabItems.length, - child: Column( - children: [ - TabBar( - tabs: tabsData.tabItems.map((tabItem) { - final ValueNotifier titleNotifier = itemContext - .dataContext - .subscribeToString(tabItem['title'] as JsonMap); - return ValueListenableBuilder( - valueListenable: titleNotifier, - builder: (context, title, child) { - return Tab(text: title ?? ''); - }, - ); - }).toList(), - ), - Builder( - builder: (context) { - final TabController tabController = DefaultTabController.of( - context, - ); - return AnimatedBuilder( - animation: tabController, - builder: (context, child) { - return itemContext.buildChild( - tabsData.tabItems[tabController.index]['child'] as String, - ); - }, - ); - }, - ), - ], - ), - ); - }, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "Tabs": { - "tabItems": [ - { - "title": { - "literalString": "Overview" - }, - "child": "text1" - }, - { - "title": { - "literalString": "Details" - }, - "child": "text2" - } - ] - } - } - }, - { - "id": "text1", - "component": { - "Text": { - "text": { - "literalString": "This is a short summary of the item." - } - } - } - }, - { - "id": "text2", - "component": { - "Text": { - "text": { - "literalString": "This is a much longer, more detailed description of the item, providing in-depth information and context. It can span multiple lines and include rich formatting if needed." - } - } - } - } - ] - ''', - ], -); diff --git a/packages/genui/lib/src/catalog/core_widgets/text_field.dart b/packages/genui/lib/src/catalog/core_widgets/text_field.dart deleted file mode 100644 index 0953b8002..000000000 --- a/packages/genui/lib/src/catalog/core_widgets/text_field.dart +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../../core/widget_utilities.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog_item.dart'; -import '../../model/data_model.dart'; -import '../../model/ui_models.dart'; -import '../../primitives/simple_items.dart'; - -final _schema = S.object( - description: 'A text input field.', - properties: { - 'text': A2uiSchemas.stringReference( - description: 'The initial value of the text field.', - ), - 'label': A2uiSchemas.stringReference(), - 'textFieldType': S.string( - enumValues: ['shortText', 'longText', 'number', 'date', 'obscured'], - ), - 'validationRegexp': S.string(), - 'onSubmittedAction': A2uiSchemas.action(), - }, -); - -extension type _TextFieldData.fromMap(JsonMap _json) { - factory _TextFieldData({ - JsonMap? text, - JsonMap? label, - String? textFieldType, - String? validationRegexp, - JsonMap? onSubmittedAction, - }) => _TextFieldData.fromMap({ - 'text': text, - 'label': label, - 'textFieldType': textFieldType, - 'validationRegexp': validationRegexp, - 'onSubmittedAction': onSubmittedAction, - }); - - JsonMap? get text => _json['text'] as JsonMap?; - JsonMap? get label => _json['label'] as JsonMap?; - String? get textFieldType => _json['textFieldType'] as String?; - String? get validationRegexp => _json['validationRegexp'] as String?; - JsonMap? get onSubmittedAction => _json['onSubmittedAction'] as JsonMap?; -} - -class _TextField extends StatefulWidget { - const _TextField({ - required this.initialValue, - this.label, - this.textFieldType, - this.validationRegexp, - required this.onChanged, - required this.onSubmitted, - }); - - final String initialValue; - final String? label; - final String? textFieldType; - final String? validationRegexp; - final void Function(String) onChanged; - final void Function(String) onSubmitted; - - @override - State<_TextField> createState() => _TextFieldState(); -} - -class _TextFieldState extends State<_TextField> { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.initialValue); - } - - @override - void didUpdateWidget(_TextField oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.initialValue != _controller.text) { - _controller.text = widget.initialValue; - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - controller: _controller, - decoration: InputDecoration(labelText: widget.label), - obscureText: widget.textFieldType == 'obscured', - keyboardType: switch (widget.textFieldType) { - 'number' => TextInputType.number, - 'longText' => TextInputType.multiline, - 'date' => TextInputType.datetime, - _ => TextInputType.text, - }, - onChanged: widget.onChanged, - onSubmitted: widget.onSubmitted, - ); - } -} - -/// A catalog item representing a Material Design text field. -/// -/// This widget allows the user to enter and edit text. The `text` parameter -/// bidirectionally binds the field's content to the data model. This is -/// analogous to Flutter's [TextField] widget. -/// -/// ## Parameters: -/// -/// - `text`: The initial value of the text field. -/// - `label`: The text to display as the label for the text field. -/// - `textFieldType`: The type of text field. Can be `shortText`, `longText`, -/// `number`, `date`, or `obscured`. -/// - `validationRegexp`: A regular expression to validate the input. -/// - `onSubmittedAction`: The action to perform when the user submits the -/// text field. -final textField = CatalogItem( - name: 'TextField', - dataSchema: _schema, - exampleData: [ - () => ''' - [ - { - "id": "root", - "component": { - "TextField": { - "text": { - "literalString": "Hello World" - }, - "label": { - "literalString": "Greeting" - } - } - } - } - ] - ''', - () => ''' - [ - { - "id": "root", - "component": { - "TextField": { - "text": { - "literalString": "password123" - }, - "label": { - "literalString": "Password" - }, - "textFieldType": "obscured" - } - } - } - ] - ''', - ], - widgetBuilder: (itemContext) { - final textFieldData = _TextFieldData.fromMap(itemContext.data as JsonMap); - final JsonMap? valueRef = textFieldData.text; - final path = valueRef?['path'] as String?; - final ValueNotifier notifier = itemContext.dataContext - .subscribeToString(valueRef); - final ValueNotifier labelNotifier = itemContext.dataContext - .subscribeToString(textFieldData.label); - - return ValueListenableBuilder( - valueListenable: notifier, - builder: (context, currentValue, child) { - return ValueListenableBuilder( - valueListenable: labelNotifier, - builder: (context, label, child) { - return _TextField( - initialValue: currentValue ?? '', - label: label, - textFieldType: textFieldData.textFieldType, - validationRegexp: textFieldData.validationRegexp, - onChanged: (newValue) { - if (path != null) { - itemContext.dataContext.update(DataPath(path), newValue); - } - }, - onSubmitted: (newValue) { - final JsonMap? actionData = textFieldData.onSubmittedAction; - if (actionData == null) { - return; - } - final actionName = actionData['name'] as String; - final List contextDefinition = - (actionData['context'] as List?) ?? []; - final JsonMap resolvedContext = resolveContext( - itemContext.dataContext, - contextDefinition, - ); - itemContext.dispatchEvent( - UserActionEvent( - name: actionName, - sourceComponentId: itemContext.id, - context: resolvedContext, - ), - ); - }, - ); - }, - ); - }, - ); - }, -); diff --git a/packages/genui/lib/src/content_generator.dart b/packages/genui/lib/src/content_generator.dart deleted file mode 100644 index 359a48886..000000000 --- a/packages/genui/lib/src/content_generator.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'model/a2ui_client_capabilities.dart'; -import 'model/a2ui_message.dart'; -import 'model/chat_message.dart'; - -/// An error produced by a [ContentGenerator]. -final class ContentGeneratorError implements Exception { - /// The error that occurred. - final Object error; - - /// The stack trace of the error. - final StackTrace? stackTrace; - - /// Creates a [ContentGeneratorError]. - const ContentGeneratorError(this.error, [this.stackTrace]); -} - -/// An abstract interface for a content generator. -/// -/// A content generator is responsible for generating UI content and handling -/// user interactions. -abstract interface class ContentGenerator { - /// A stream of A2UI messages produced by the generator. - /// - /// The `GenUiConversation` will listen to this stream and forward messages - /// to the `A2uiMessageProcessor`. - Stream get a2uiMessageStream; - - /// A stream of text responses from the agent. - Stream get textResponseStream; - - /// A stream of errors from the agent. - Stream get errorStream; - - /// Whether the content generator is currently processing a request. - ValueListenable get isProcessing; - - /// Sends a message to the content source to generate a response, optionally - /// including the previous conversation history. - /// - /// Some implementations, particularly those that manage their own state - /// (stateful), may ignore the `history` parameter. - Future sendRequest( - ChatMessage message, { - Iterable? history, - A2UiClientCapabilities? clientCapabilities, - }); - - /// Disposes of the resources used by this generator. - void dispose(); -} diff --git a/packages/genui/lib/src/core/a2ui_message_processor.dart b/packages/genui/lib/src/core/a2ui_message_processor.dart deleted file mode 100644 index 8547bfc93..000000000 --- a/packages/genui/lib/src/core/a2ui_message_processor.dart +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; - -import '../model/a2ui_message.dart'; -import '../model/catalog.dart'; -import '../model/chat_message.dart'; -import '../model/data_model.dart'; -import '../model/ui_models.dart'; -import '../primitives/logging.dart'; - -/// A sealed class representing an update to the UI managed by -/// [A2uiMessageProcessor]. -/// -/// This class has three subclasses: [SurfaceAdded], [SurfaceUpdated], and -/// [SurfaceRemoved]. -sealed class GenUiUpdate { - /// Creates a [GenUiUpdate] for the given [surfaceId]. - const GenUiUpdate(this.surfaceId); - - /// The ID of the surface that was updated. - final String surfaceId; -} - -/// Fired when a new surface is created. -class SurfaceAdded extends GenUiUpdate { - /// Creates a [SurfaceAdded] event for the given [surfaceId] and - /// [definition]. - const SurfaceAdded(super.surfaceId, this.definition); - - /// The definition of the new surface. - final UiDefinition definition; -} - -/// Fired when an existing surface is modified. -class SurfaceUpdated extends GenUiUpdate { - /// Creates a [SurfaceUpdated] event for the given [surfaceId] and - /// [definition]. - const SurfaceUpdated(super.surfaceId, this.definition); - - /// The new definition of the surface. - final UiDefinition definition; -} - -/// Fired when a surface is deleted. -class SurfaceRemoved extends GenUiUpdate { - /// Creates a [SurfaceRemoved] event for the given [surfaceId]. - const SurfaceRemoved(super.surfaceId); -} - -/// An interface for a class that hosts UI surfaces. -/// -/// This is used by `GenUiSurface` to get the UI definition for a surface, -/// listen for updates, and notify the host of user interactions. -abstract interface class GenUiHost { - /// A stream of updates for the surfaces managed by this host. - Stream get surfaceUpdates; - - /// Returns a [ValueNotifier] for the surface with the given [surfaceId]. - ValueNotifier getSurfaceNotifier(String surfaceId); - - /// The catalogs of UI components available to the AI. - Iterable get catalogs; - - /// A map of data models for storing the UI state of each surface. - Map get dataModels; - - /// The data model for storing the UI state for a given surface. - DataModel dataModelForSurface(String surfaceId); - - /// A callback to handle an action from a surface. - void handleUiEvent(UiEvent event); -} - -/// Manages the state of all dynamic UI surfaces. -/// -/// This class is the core state manager for the dynamic UI. It maintains a map -/// of all active UI "surfaces", where each surface is represented by a -/// `UiDefinition`. It provides the tools (`surfaceUpdate`, `deleteSurface`, -/// `beginRendering`) that the AI uses to manipulate the UI. It exposes a stream -/// of `GenUiUpdate` events so that the application can react to changes. -class A2uiMessageProcessor implements GenUiHost { - /// Creates a new [A2uiMessageProcessor] with a list of supported widget - /// catalogs. - A2uiMessageProcessor({required this.catalogs}); - - @override - final Iterable catalogs; - - final _surfaces = >{}; - final _surfaceUpdates = StreamController.broadcast(); - final _onSubmit = StreamController.broadcast(); - - final _dataModels = {}; - - @override - Map get dataModels => Map.unmodifiable(_dataModels); - - @override - DataModel dataModelForSurface(String surfaceId) { - return _dataModels.putIfAbsent(surfaceId, DataModel.new); - } - - /// A map of all the surfaces managed by this manager, keyed by surface ID. - Map> get surfaces => _surfaces; - - @override - Stream get surfaceUpdates => _surfaceUpdates.stream; - - /// A stream of user input messages generated from UI interactions. - Stream get onSubmit => _onSubmit.stream; - - @override - void handleUiEvent(UiEvent event) { - if (event is! UserActionEvent) { - // Or handle other event types if necessary - return; - } - - final String eventJsonString = jsonEncode({'userAction': event.toMap()}); - _onSubmit.add(UserUiInteractionMessage.text(eventJsonString)); - } - - @override - ValueNotifier getSurfaceNotifier(String surfaceId) { - if (!_surfaces.containsKey(surfaceId)) { - genUiLogger.fine('Adding new surface $surfaceId'); - } else { - genUiLogger.fine('Fetching surface notifier for $surfaceId'); - } - return _surfaces.putIfAbsent( - surfaceId, - () => ValueNotifier(null), - ); - } - - /// Disposes of the resources used by this manager. - void dispose() { - _surfaceUpdates.close(); - _onSubmit.close(); - for (final ValueNotifier notifier in _surfaces.values) { - notifier.dispose(); - } - } - - /// Handles an [A2uiMessage] and updates the UI accordingly. - void handleMessage(A2uiMessage message) { - switch (message) { - case SurfaceUpdate(): - final String surfaceId = message.surfaceId; - final ValueNotifier notifier = getSurfaceNotifier( - surfaceId, - ); - - UiDefinition uiDefinition = - notifier.value ?? UiDefinition(surfaceId: surfaceId); - final Map newComponents = Map.of( - uiDefinition.components, - ); - for (final Component component in message.components) { - newComponents[component.id] = component; - } - uiDefinition = uiDefinition.copyWith(components: newComponents); - notifier.value = uiDefinition; - - // Notify UI ONLY if rendering has begun (i.e., rootComponentId is set) - if (uiDefinition.rootComponentId != null) { - genUiLogger.info('Updating surface $surfaceId'); - _surfaceUpdates.add(SurfaceUpdated(surfaceId, uiDefinition)); - } else { - genUiLogger.info( - 'Caching components for surface $surfaceId (pre-rendering)', - ); - } - case BeginRendering(): - final String surfaceId = message.surfaceId; - dataModelForSurface(surfaceId); - final ValueNotifier notifier = getSurfaceNotifier( - surfaceId, - ); - - // Update the definition with the root component - final UiDefinition uiDefinition = - notifier.value ?? UiDefinition(surfaceId: surfaceId); - final UiDefinition newUiDefinition = uiDefinition.copyWith( - rootComponentId: message.root, - catalogId: message.catalogId, - ); - notifier.value = newUiDefinition; - - genUiLogger.info('Creating and rendering surface $surfaceId'); - _surfaceUpdates.add(SurfaceAdded(surfaceId, newUiDefinition)); - case DataModelUpdate(): - final String path = message.path ?? '/'; - genUiLogger.info( - 'Updating data model for surface ${message.surfaceId} at path ' - '$path with contents:\n' - '${const JsonEncoder.withIndent(' ').convert(message.contents)}', - ); - final DataModel dataModel = dataModelForSurface(message.surfaceId); - dataModel.update(DataPath(path), message.contents); - - // Notify UI of an update if the surface is already rendering - final ValueNotifier notifier = getSurfaceNotifier( - message.surfaceId, - ); - final UiDefinition? uiDefinition = notifier.value; - if (uiDefinition != null && uiDefinition.rootComponentId != null) { - _surfaceUpdates.add(SurfaceUpdated(message.surfaceId, uiDefinition)); - } - case SurfaceDeletion(): - final String surfaceId = message.surfaceId; - if (_surfaces.containsKey(surfaceId)) { - genUiLogger.info('Deleting surface $surfaceId'); - final ValueNotifier? notifier = _surfaces.remove( - surfaceId, - ); - notifier?.dispose(); - _dataModels.remove(surfaceId); - _surfaceUpdates.add(SurfaceRemoved(surfaceId)); - } - } - } -} diff --git a/packages/genui/lib/src/core/genui_surface.dart b/packages/genui/lib/src/core/genui_surface.dart deleted file mode 100644 index e7b6685c5..000000000 --- a/packages/genui/lib/src/core/genui_surface.dart +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -import '../core/a2ui_message_processor.dart'; -import '../model/catalog.dart'; -import '../model/catalog_item.dart'; -import '../model/data_model.dart'; -import '../model/tools.dart'; -import '../model/ui_models.dart'; -import '../primitives/constants.dart'; -import '../primitives/logging.dart'; -import '../primitives/simple_items.dart'; - -/// A callback for when a user interacts with a widget. -typedef UiEventCallback = void Function(UiEvent event); - -/// A widget that builds a UI dynamically from a JSON-like definition. -/// -/// It reports user interactions via the [host]. -class GenUiSurface extends StatefulWidget { - /// Creates a new [GenUiSurface]. - const GenUiSurface({ - super.key, - required this.host, - required this.surfaceId, - this.defaultBuilder, - }); - - /// The manager that holds the state of the UI. - final GenUiHost host; - - /// The ID of the surface that this UI belongs to. - final String surfaceId; - - /// A builder for the widget to display when the surface has no definition. - final WidgetBuilder? defaultBuilder; - - @override - State createState() => _GenUiSurfaceState(); -} - -class _GenUiSurfaceState extends State { - @override - Widget build(BuildContext context) { - genUiLogger.fine('Outer Building surface ${widget.surfaceId}'); - return ValueListenableBuilder( - valueListenable: widget.host.getSurfaceNotifier(widget.surfaceId), - builder: (context, definition, child) { - genUiLogger.fine('Building surface ${widget.surfaceId}'); - if (definition == null) { - genUiLogger.info('Surface ${widget.surfaceId} has no definition.'); - return widget.defaultBuilder?.call(context) ?? - const SizedBox.shrink(); - } - final String? rootId = definition.rootComponentId; - if (rootId == null || definition.components.isEmpty) { - genUiLogger.warning('Surface ${widget.surfaceId} has no widgets.'); - return const SizedBox.shrink(); - } - - final Catalog? catalog = _findCatalogForDefinition(definition); - if (catalog == null) { - return Container(); - } - - return _buildWidget( - definition, - catalog, - rootId, - DataContext(widget.host.dataModelForSurface(widget.surfaceId), '/'), - ); - }, - ); - } - - /// The main recursive build function. - /// It reads a widget definition and its current state from - /// `widget.definition` - /// and constructs the corresponding Flutter widget. - Widget _buildWidget( - UiDefinition definition, - Catalog catalog, - String widgetId, - DataContext dataContext, - ) { - Component? data = definition.components[widgetId]; - if (data == null) { - genUiLogger.severe('Widget with id: $widgetId not found.'); - return Placeholder(child: Text('Widget with id: $widgetId not found.')); - } - - final JsonMap widgetData = data.componentProperties; - genUiLogger.finest('Building widget $widgetId'); - return catalog.buildWidget( - CatalogItemContext( - id: widgetId, - data: widgetData, - buildChild: (String childId, [DataContext? childDataContext]) => - _buildWidget( - definition, - catalog, - childId, - childDataContext ?? dataContext, - ), - dispatchEvent: _dispatchEvent, - buildContext: context, - dataContext: dataContext, - getComponent: (String componentId) => - definition.components[componentId], - surfaceId: widget.surfaceId, - ), - ); - } - - void _dispatchEvent(UiEvent event) { - if (event is UserActionEvent && event.name == 'showModal') { - final UiDefinition? definition = widget.host - .getSurfaceNotifier(widget.surfaceId) - .value; - if (definition == null) return; - - final Catalog? catalog = _findCatalogForDefinition(definition); - if (catalog == null) { - genUiLogger.severe( - 'Cannot show modal for surface "${widget.surfaceId}" because ' - 'a catalog was not found.', - ); - return; - } - - final modalId = event.context['modalId'] as String; - final Component? modalComponent = definition.components[modalId]; - if (modalComponent == null) return; - final contentChildId = - (modalComponent.componentProperties['Modal'] as Map)['contentChild'] - as String; - showModalBottomSheet( - context: context, - builder: (context) => _buildWidget( - definition, - catalog, - contentChildId, - DataContext(widget.host.dataModelForSurface(widget.surfaceId), '/'), - ), - ); - return; - } - - // The event comes in without a surfaceId, which we add here. - final Map eventMap = { - ...event.toMap(), - surfaceIdKey: widget.surfaceId, - }; - final UiEvent newEvent = event is UserActionEvent - ? UserActionEvent.fromMap(eventMap) - : UiEvent.fromMap(eventMap); - widget.host.handleUiEvent(newEvent); - } - - Catalog? _findCatalogForDefinition(UiDefinition definition) { - final String catalogId = definition.catalogId ?? standardCatalogId; - final Catalog? catalog = widget.host.catalogs.firstWhereOrNull( - (c) => c.catalogId == catalogId, - ); - - if (catalog == null) { - genUiLogger.severe( - 'Catalog with id "$catalogId" not found for surface ' - '"${widget.surfaceId}". Ensure the catalog is provided to ' - 'A2uiMessageProcessor.', - ); - } - return catalog; - } -} diff --git a/packages/genui/lib/src/core/prompt_fragments.dart b/packages/genui/lib/src/core/prompt_fragments.dart deleted file mode 100644 index d44e665f2..000000000 --- a/packages/genui/lib/src/core/prompt_fragments.dart +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A collection of prompt fragments for use with GenUI. -class GenUiPromptFragments { - /// A basic chat prompt fragment. - static const String basicChat = ''' - -# Outputting UI information - -Use the provided tools to respond to the user using rich UI elements. - -Important considerations: -- When you are asking for information from the user, you should always include - at least one submit button of some kind or another submitting element so that - the user can indicate that they are done providing information. -- After you have modified the UI, be sure to use the provideFinalOutput to give - control back to the user so they can respond. -'''; -} diff --git a/packages/genui/lib/src/core/ui_tools.dart b/packages/genui/lib/src/core/ui_tools.dart deleted file mode 100644 index 2b74abef6..000000000 --- a/packages/genui/lib/src/core/ui_tools.dart +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:json_schema_builder/json_schema_builder.dart'; - -import '../model/a2ui_message.dart'; -import '../model/a2ui_schemas.dart'; -import '../model/catalog.dart'; -import '../model/tools.dart'; -import '../model/ui_models.dart'; -import '../primitives/simple_items.dart'; - -/// An [AiTool] for adding or updating a UI surface. -/// -/// This tool allows the AI to create a new UI surface or update an existing -/// one with a new definition. -class SurfaceUpdateTool extends AiTool { - /// Creates an [SurfaceUpdateTool]. - SurfaceUpdateTool({required this.handleMessage, required Catalog catalog}) - : super( - name: 'surfaceUpdate', - description: 'Updates a surface with a new set of components.', - parameters: A2uiSchemas.surfaceUpdateSchema(catalog), - ); - - /// The callback to invoke when adding or updating a surface. - final void Function(A2uiMessage message) handleMessage; - - @override - Future invoke(JsonMap args) async { - final surfaceId = args[surfaceIdKey] as String; - final List components = (args['components'] as List).map((e) { - final component = e as JsonMap; - return Component( - id: component['id'] as String, - componentProperties: component['component'] as JsonMap, - weight: (component['weight'] as num?)?.toInt(), - ); - }).toList(); - handleMessage(SurfaceUpdate(surfaceId: surfaceId, components: components)); - return { - surfaceIdKey: surfaceId, - 'status': 'UI Surface $surfaceId updated.', - }; - } -} - -/// An [AiTool] for deleting a UI surface. -/// -/// This tool allows the AI to remove a UI surface that is no longer needed. -class DeleteSurfaceTool extends AiTool { - /// Creates a [DeleteSurfaceTool]. - DeleteSurfaceTool({required this.handleMessage}) - : super( - name: 'deleteSurface', - description: 'Removes a UI surface that is no longer needed.', - parameters: S.object( - properties: { - surfaceIdKey: S.string( - description: - 'The unique identifier for the UI surface to remove.', - ), - }, - required: [surfaceIdKey], - ), - ); - - /// The callback to invoke when deleting a surface. - final void Function(A2uiMessage message) handleMessage; - - @override - Future invoke(JsonMap args) async { - final surfaceId = args[surfaceIdKey] as String; - handleMessage(SurfaceDeletion(surfaceId: surfaceId)); - return {'status': 'Surface $surfaceId deleted.'}; - } -} - -/// An [AiTool] for signaling the client to begin rendering. -/// -/// This tool allows the AI to specify the root component of a UI surface. -class BeginRenderingTool extends AiTool { - /// Creates a [BeginRenderingTool]. - BeginRenderingTool({required this.handleMessage, this.catalogId}) - : super( - name: 'beginRendering', - description: - 'Signals the client to begin rendering a surface with a ' - 'root component.', - parameters: A2uiSchemas.beginRenderingSchemaNoCatalogId(), - ); - - /// The callback to invoke when signaling to begin rendering. - final void Function(A2uiMessage message) handleMessage; - - /// The ID of the catalog to use for rendering this surface. - final String? catalogId; - - @override - Future invoke(JsonMap args) async { - final surfaceId = args[surfaceIdKey] as String; - final root = args['root'] as String; - handleMessage( - BeginRendering(surfaceId: surfaceId, root: root, catalogId: catalogId), - ); - return { - 'status': 'Surface $surfaceId rendered and waiting for user input.', - }; - } -} diff --git a/packages/genui/lib/src/core/widget_utilities.dart b/packages/genui/lib/src/core/widget_utilities.dart deleted file mode 100644 index 70b9ac25e..000000000 --- a/packages/genui/lib/src/core/widget_utilities.dart +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import '../model/data_model.dart'; -import '../primitives/logging.dart'; -import '../primitives/simple_items.dart'; - -/// A builder widget that simplifies handling of nullable `ValueListenable`s. -/// -/// This widget listens to a `ValueListenable` and rebuilds its child -/// whenever the value changes. If the value is `null`, it returns a -/// `SizedBox.shrink()`, effectively hiding the child. If the value is not -/// `null`, it calls the `builder` function with the non-nullable value. -class OptionalValueBuilder extends StatelessWidget { - /// The `ValueListenable` to listen to. - final ValueListenable listenable; - - /// The builder function to call when the value is not `null`. - final Widget Function(BuildContext context, T value) builder; - - /// Creates an `OptionalValueBuilder`. - const OptionalValueBuilder({ - super.key, - required this.listenable, - required this.builder, - }); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: listenable, - builder: (context, value, _) { - if (value == null) return const SizedBox.shrink(); - return builder(context, value); - }, - ); - } -} - -/// Extension methods for [DataContext] to simplify data binding. -extension DataContextExtensions on DataContext { - /// Subscribes to a value, which can be a literal or a data-bound path. - ValueNotifier subscribeToValue(JsonMap? ref, String literalKey) { - genUiLogger.info( - 'DataContext.subscribeToValue: ref=$ref, literalKey=$literalKey', - ); - if (ref == null) return ValueNotifier(null); - final path = ref['path'] as String?; - final Object? literal = ref[literalKey]; - - if (path != null) { - final dataPath = DataPath(path); - if (literal != null) { - update(dataPath, literal); - } - return subscribe(dataPath); - } - - return ValueNotifier(literal as T?); - } - - /// Subscribes to a string value, which can be a literal or a data-bound path. - ValueNotifier subscribeToString(JsonMap? ref) { - return subscribeToValue(ref, 'literalString'); - } - - /// Subscribes to a boolean value, which can be a literal or a data-bound - /// path. - ValueNotifier subscribeToBool(JsonMap? ref) { - return subscribeToValue(ref, 'literalBoolean'); - } - - /// Subscribes to a list of objects, which can be a literal or a data-bound - /// path. - ValueNotifier?> subscribeToObjectArray(JsonMap? ref) { - return subscribeToValue>(ref, 'literalArray'); - } -} - -/// Resolves a context map definition against a [DataContext]. -/// -JsonMap resolveContext( - DataContext dataContext, - List contextDefinitions, -) { - final resolved = {}; - for (final contextEntry in contextDefinitions) { - final entry = contextEntry as JsonMap; - final key = entry['key']! as String; - final value = entry['value'] as JsonMap; - if (value.containsKey('path')) { - resolved[key] = dataContext.getValue(DataPath(value['path'] as String)); - } else if (value.containsKey('literalString')) { - resolved[key] = value['literalString']; - } else if (value.containsKey('literalNumber')) { - resolved[key] = value['literalNumber']; - } else if (value.containsKey('literalBoolean')) { - resolved[key] = value['literalBoolean']; - } - } - return resolved; -} diff --git a/packages/genui/lib/src/development_utilities/catalog_view.dart b/packages/genui/lib/src/development_utilities/catalog_view.dart index bf962c106..4c095d6e9 100644 --- a/packages/genui/lib/src/development_utilities/catalog_view.dart +++ b/packages/genui/lib/src/development_utilities/catalog_view.dart @@ -8,16 +8,17 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import '../core/a2ui_message_processor.dart'; -import '../core/genui_surface.dart'; +import '../engine/surface_controller.dart'; import '../model/a2ui_message.dart'; import '../model/catalog.dart'; import '../model/catalog_item.dart'; import '../model/chat_message.dart'; import '../model/ui_models.dart'; +import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; +import '../widgets/surface.dart'; -/// A widget that displays a GenUI catalog widgets. +/// A widget that displays a catalog of GenUI components. /// /// This widget is intended for development and debugging purposes. /// @@ -35,7 +36,7 @@ class DebugCatalogView extends StatefulWidget { final Catalog catalog; /// A callback for when a user submits an action. - final ValueChanged? onSubmit; + final ValueChanged? onSubmit; /// If provided, constrains each item to the given height. final double? itemHeight; @@ -45,18 +46,18 @@ class DebugCatalogView extends StatefulWidget { } class _DebugCatalogViewState extends State { - late final A2uiMessageProcessor _a2uiMessageProcessor; + late final SurfaceController _surfaceController; final surfaceIds = []; - late final StreamSubscription? _subscription; + late final StreamSubscription? _subscription; @override void initState() { super.initState(); final Catalog catalog = widget.catalog; - _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [catalog]); + _surfaceController = SurfaceController(catalogs: [widget.catalog]); if (widget.onSubmit != null) { - _subscription = _a2uiMessageProcessor.onSubmit.listen(widget.onSubmit); + _subscription = _surfaceController.onSubmit.listen(widget.onSubmit); } else { _subscription = null; } @@ -80,26 +81,25 @@ class _DebugCatalogViewState extends State { rootComponent = components.firstWhereOrNull((c) => c.id == 'root'); if (rootComponent == null) { - debugPrint( + genUiLogger.info( 'Skipping example for ${item.name} because it is missing a root ' 'component.', ); continue; } - _a2uiMessageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + _surfaceController.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), ); - _a2uiMessageProcessor.handleMessage( - BeginRendering( - surfaceId: surfaceId, - root: rootComponent.id, - catalogId: catalog.catalogId, - ), + _surfaceController.handleMessage( + CreateSurface(surfaceId: surfaceId, catalogId: catalog.catalogId!), ); surfaceIds.add(surfaceId); - } catch (e, s) { - debugPrint('Failed to load example for "${item.name}":\n$e\n$s'); + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Failed to load example for "${item.name}":\n' + '$exception\n$stackTrace', + ); throw Exception( 'Failed to load example for "${item.name}". Check logs for ' 'details.', @@ -112,7 +112,7 @@ class _DebugCatalogViewState extends State { @override void dispose() { _subscription?.cancel(); - _a2uiMessageProcessor.dispose(); + _surfaceController.dispose(); super.dispose(); } @@ -122,9 +122,8 @@ class _DebugCatalogViewState extends State { itemCount: surfaceIds.length, itemBuilder: (BuildContext context, int index) { final String surfaceId = surfaceIds[index]; - final surfaceWidget = GenUiSurface( - host: _a2uiMessageProcessor, - surfaceId: surfaceId, + final surfaceWidget = Surface( + surfaceContext: _surfaceController.contextFor(surfaceId), ); return Card( color: Theme.of(context).colorScheme.secondaryContainer, diff --git a/packages/genui/lib/src/engine/cleanup_strategy.dart b/packages/genui/lib/src/engine/cleanup_strategy.dart new file mode 100644 index 000000000..6122882ff --- /dev/null +++ b/packages/genui/lib/src/engine/cleanup_strategy.dart @@ -0,0 +1,35 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A strategy for cleaning up surfaces. +abstract interface class SurfaceCleanupStrategy { + /// Determines which surfaces should be removed. + /// + /// [surfaceOrder] is the list of surfaces in creation/update order (oldest first). + /// Returns a list of surface IDs to remove. + List cleanup(List surfaceOrder); +} + +/// A manual cleanup strategy that does nothing. +class ManualCleanupStrategy implements SurfaceCleanupStrategy { + const ManualCleanupStrategy(); + + @override + List cleanup(List surfaceOrder) => const []; +} + +/// A cleanup strategy that keeps the latest N surfaces. +class KeepLastNCleanupStrategy implements SurfaceCleanupStrategy { + const KeepLastNCleanupStrategy(this.n); + + final int n; + + @override + List cleanup(List surfaceOrder) { + if (surfaceOrder.length > n) { + return surfaceOrder.sublist(0, surfaceOrder.length - n); + } + return const []; + } +} diff --git a/packages/genui/lib/src/engine/data_model_store.dart b/packages/genui/lib/src/engine/data_model_store.dart new file mode 100644 index 000000000..51f347dc9 --- /dev/null +++ b/packages/genui/lib/src/engine/data_model_store.dart @@ -0,0 +1,47 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../model/data_model.dart'; + +/// Manages the data models for surfaces. +class DataModelStore { + final Map _dataModels = {}; + final Set _attachedSurfaces = {}; + + DataModel getDataModel(String surfaceId) { + return _dataModels.putIfAbsent(surfaceId, DataModel.new); + } + + void removeDataModel(String surfaceId) { + final DataModel? model = _dataModels.remove(surfaceId); + model?.dispose(); + _attachedSurfaces.remove(surfaceId); + } + + void attachSurface(String surfaceId) { + _attachedSurfaces.add(surfaceId); + } + + void detachSurface(String surfaceId) { + _attachedSurfaces.remove(surfaceId); + } + + Map getClientDataSnapshot() { + final result = {}; + for (final String surfaceId in _attachedSurfaces) { + if (_dataModels.containsKey(surfaceId)) { + result[surfaceId] = _dataModels[surfaceId]!.data; + } + } + return {'version': 'v0.9', 'surfaces': result}; + } + + Map get dataModels => Map.unmodifiable(_dataModels); + + void dispose() { + for (final DataModel model in _dataModels.values) { + model.dispose(); + } + } +} diff --git a/packages/genui/lib/src/engine/surface_controller.dart b/packages/genui/lib/src/engine/surface_controller.dart new file mode 100644 index 000000000..4e1e650f6 --- /dev/null +++ b/packages/genui/lib/src/engine/surface_controller.dart @@ -0,0 +1,345 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; + +import '../interfaces/a2ui_message_sink.dart'; +import '../interfaces/surface_context.dart'; +import '../interfaces/surface_host.dart'; +import '../model/a2ui_message.dart'; +import '../model/catalog.dart'; +import '../model/chat_message.dart'; +import '../model/data_model.dart'; +import '../model/ui_models.dart'; +import '../primitives/constants.dart'; +import '../primitives/logging.dart'; +import 'cleanup_strategy.dart'; +import 'data_model_store.dart'; +import 'surface_registry.dart' as surface_reg; + +/// The runtime controller for the GenUI system. +/// +/// Orchestrates the lifecycle of UI surfaces, manages communication with the +/// AI service, and handles data model updates. +interface class SurfaceController implements SurfaceHost, A2uiMessageSink { + /// Creates a [SurfaceController]. + /// + /// The [catalogs] parameter defines the set of component catalogs available + /// for use by surfaces managed by this controller. + /// + /// The [cleanupStrategy] determines when and how surfaces are removed from + /// the registry to free up resources. + /// + /// The [pendingUpdateTimeout] specifies how long to wait for a surface + /// creation message before discarding buffered updates for that surface. + SurfaceController({ + required this.catalogs, + this.cleanupStrategy = const ManualCleanupStrategy(), + this.pendingUpdateTimeout = const Duration(minutes: 1), + }); + + /// The catalogs available to surfaces in this engine. + final Iterable catalogs; + + /// The strategy used to clean up unused surfaces. + final SurfaceCleanupStrategy cleanupStrategy; + + /// The timeout for pending updates waiting for a surface creation. + final Duration pendingUpdateTimeout; + + late final surface_reg.SurfaceRegistry _registry = + surface_reg.SurfaceRegistry(); + late final DataModelStore _store = DataModelStore(); + + final _onSubmit = StreamController.broadcast(); + final _pendingUpdates = >{}; + final _pendingUpdateTimers = {}; + + // Expose registry events as surface updates + @override + Stream get surfaceUpdates => _registry.events.map((e) { + switch (e) { + case surface_reg.SurfaceAdded(): + return SurfaceAdded(e.surfaceId, e.definition); + case surface_reg.SurfaceUpdated(): + return ComponentsUpdated(e.surfaceId, e.definition); + case surface_reg.SurfaceRemoved(): + return SurfaceRemoved(e.surfaceId); + } + }); + + /// A stream of messages to be submitted to the AI service. + /// + /// This includes user actions and validation errors. + Stream get onSubmit => _onSubmit.stream; + + /// The IDs of the currently active surfaces. + Iterable get activeSurfaceIds => _registry.surfaceOrder; + + @override + SurfaceContext contextFor(String surfaceId) { + return _ControllerContext(this, surfaceId); + } + + @override + ValueListenable watchSurface(String surfaceId) { + return _registry.watchSurface(surfaceId); + } + + /// The registry of surfaces managed by this controller. + @visibleForTesting + surface_reg.SurfaceRegistry get registry => _registry; + + /// The store of data models managed by this controller. + @visibleForTesting + DataModelStore get store => _store; + + /// Process an [message] from the AI service. + /// + /// Decodes the message and updates the state of the relevant surface, + /// provided the message passes validation. + /// + /// If validation fails, a [A2uiValidationException] is caught and logged, + /// and an error message is sent back via [onSubmit]. + @override + void handleMessage(A2uiMessage message) { + genUiLogger.info( + 'SurfaceController.handleMessage received: ${message.runtimeType}', + ); + + try { + _handleMessageInternal(message); + } on A2uiValidationException catch (e) { + genUiLogger.warning('Validation failed for surface ${e.surfaceId}: $e'); + reportError(e, StackTrace.current); + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Error handling message: $message', + exception, + stackTrace, + ); + reportError(exception, stackTrace); + } + } + + /// Reports an error to the AI service. + void reportError(Object error, StackTrace? stack) { + var errorCode = 'RUNTIME_ERROR'; + var message = error.toString(); + String? surfaceId; + String? path; + + if (error is A2uiValidationException) { + errorCode = 'VALIDATION_FAILED'; + message = error.message; + surfaceId = error.surfaceId; + path = error.path; + } + + final Map errorMsg = { + 'version': 'v0.9', + 'error': { + 'code': errorCode, + 'surfaceId': ?surfaceId, + 'path': ?path, + 'message': message, + }, + }; + _onSubmit.add( + ChatMessage.user( + '', + parts: [UiInteractionPart.create(jsonEncode(errorMsg))], + ), + ); + } + + void _handleMessageInternal(A2uiMessage message) { + switch (message) { + case CreateSurface(): + final String surfaceId = message.surfaceId; + if (surfaceId.isEmpty) { + throw A2uiValidationException( + 'Surface ID cannot be empty', + surfaceId: surfaceId, + path: 'surfaceId', + ); + } + + final List? pending = _pendingUpdates.remove(surfaceId); + _pendingUpdateTimers.remove(surfaceId)?.cancel(); + + _store.getDataModel(surfaceId); // Ensure model exists + + final SurfaceDefinition? existing = _registry.getSurface(surfaceId); + final SurfaceDefinition newDefinition = + (existing ?? SurfaceDefinition(surfaceId: surfaceId)).copyWith( + catalogId: message.catalogId, + theme: message.theme, + ); + + if (message.sendDataModel) { + _store.attachSurface(surfaceId); + } else { + _store.detachSurface(surfaceId); + } + + _registry.updateSurface( + surfaceId, + newDefinition, + isNew: existing == null, + ); + + final Catalog? catalog = _findCatalogForDefinition(newDefinition); + if (catalog != null) { + newDefinition.validate(catalog.definition); + } + + _enforceCleanupPolicy(); + + if (pending != null) { + for (final A2uiMessage msg in pending) { + _handleMessageInternal(msg); + } + } + + case UpdateComponents(): + final String surfaceId = message.surfaceId; + if (!_registry.hasSurface(surfaceId)) { + _bufferMessage(surfaceId, message); + return; + } + + final SurfaceDefinition current = _registry.getSurface(surfaceId)!; + final Map newComponents = Map.of(current.components); + for (final Component component in message.components) { + newComponents[component.id] = component; + } + + _registry.updateSurface( + surfaceId, + current.copyWith(components: newComponents), + ); + + final SurfaceDefinition updatedDefinition = _registry.getSurface( + surfaceId, + )!; + final Catalog? catalog = _findCatalogForDefinition(updatedDefinition); + if (catalog != null) { + updatedDefinition.validate(catalog.definition); + } + + case UpdateDataModel(): + final String surfaceId = message.surfaceId; + if (!_registry.hasSurface(surfaceId)) { + _bufferMessage(surfaceId, message); + return; + } + + final DataModel model = _store.getDataModel(surfaceId); + model.update(message.path, message.value); + + // Trigger generic update on surface to refresh UI + final SurfaceDefinition current = _registry.getSurface(surfaceId)!; + _registry.updateSurface(surfaceId, current); + + case DeleteSurface(): + final String surfaceId = message.surfaceId; + _pendingUpdates.remove(surfaceId); + _pendingUpdateTimers.remove(surfaceId)?.cancel(); + _registry.removeSurface(surfaceId); + _store.removeDataModel(surfaceId); + } + } + + void _bufferMessage(String surfaceId, A2uiMessage message) { + _pendingUpdates.putIfAbsent(surfaceId, () => []).add(message); + if (!_pendingUpdateTimers.containsKey(surfaceId)) { + _pendingUpdateTimers[surfaceId] = Timer(pendingUpdateTimeout, () { + _pendingUpdates.remove(surfaceId); + _pendingUpdateTimers.remove(surfaceId); + }); + } + } + + void _enforceCleanupPolicy() { + final List toRemove = cleanupStrategy.cleanup( + _registry.surfaceOrder, + ); + for (final id in toRemove) { + _registry.removeSurface(id); + _store.removeDataModel(id); + } + } + + /// Handles a UI event from a surface. + /// + /// Converts the event into a [ChatMessage] and adds it to the [onSubmit] + /// stream. + void handleUiEvent(UiEvent event) { + if (event is! UserActionEvent) return; + _onSubmit.add( + ChatMessage.user( + '', + parts: [ + UiInteractionPart.create( + jsonEncode({'version': 'v0.9', 'action': event.toMap()}), + ), + ], + ), + ); + } + + Catalog? _findCatalogForDefinition(SurfaceDefinition definition) { + final String catalogId = definition.catalogId ?? basicCatalogId; + genUiLogger.fine( + 'Finding catalog for $catalogId in ' + '${catalogs.map((c) => c.catalogId).toList()}', + ); + return catalogs.firstWhereOrNull((c) => c.catalogId == catalogId); + } + + /// Disposes of the controller and releases all resources. + /// + /// Closes the [onSubmit] stream and cancels any pending timers. + void dispose() { + _registry.dispose(); + _store.dispose(); + _onSubmit.close(); + for (final Timer timer in _pendingUpdateTimers.values) { + timer.cancel(); + } + } +} + +class _ControllerContext implements SurfaceContext { + _ControllerContext(this._controller, this.surfaceId); + final SurfaceController _controller; + + @override + final String surfaceId; + + @override + ValueListenable get definition => + _controller.registry.watchSurface(surfaceId); + + @override + DataModel get dataModel => _controller.store.getDataModel(surfaceId); + + @override + Iterable get catalogs => _controller.catalogs; + + @override + void handleUiEvent(UiEvent event) { + _controller.handleUiEvent(event); + } + + @override + void reportError(Object error, StackTrace? stack) { + _controller.reportError(error, stack); + } +} diff --git a/packages/genui/lib/src/engine/surface_registry.dart b/packages/genui/lib/src/engine/surface_registry.dart new file mode 100644 index 000000000..72285889c --- /dev/null +++ b/packages/genui/lib/src/engine/surface_registry.dart @@ -0,0 +1,129 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../model/ui_models.dart'; +import '../primitives/logging.dart'; + +/// Events emitted by the [SurfaceRegistry]. +sealed class RegistryEvent {} + +/// An event indicating that a new surface has been added. +class SurfaceAdded extends RegistryEvent { + /// Creates a [SurfaceAdded] event. + SurfaceAdded(this.surfaceId, this.definition); + final String surfaceId; + final SurfaceDefinition definition; +} + +/// An event indicating that a surface has been removed. +class SurfaceRemoved extends RegistryEvent { + /// Creates a [SurfaceRemoved] event. + SurfaceRemoved(this.surfaceId); + final String surfaceId; +} + +/// An event indicating that a surface has been updated. +class SurfaceUpdated extends RegistryEvent { + /// Creates a [SurfaceUpdated] event. + SurfaceUpdated(this.surfaceId, this.definition); + final String surfaceId; + final SurfaceDefinition definition; +} + +/// Manages the lifecycle and storage of [SurfaceDefinition]s. +class SurfaceRegistry { + final Map> _surfaces = {}; + // Track creation/update order for cleanup policies + final List _surfaceOrder = []; + final StreamController _eventController = + StreamController.broadcast(); + + /// The stream of registry events. + Stream get events => _eventController.stream; + + /// The list of surface IDs in the order they were created or updated. + /// + /// This is used by cleanup strategies to determine which surfaces to remove. + List get surfaceOrder => List.unmodifiable(_surfaceOrder); + + /// Returns a [ValueListenable] that tracks the definition of the surface + /// with the given [surfaceId]. + /// + /// If the surface does not exist, a new notifier is created with a null + /// value. + ValueListenable watchSurface(String surfaceId) { + if (!_surfaces.containsKey(surfaceId)) { + genUiLogger.fine('Adding new surface $surfaceId'); + } else { + genUiLogger.fine('Fetching surface notifier for $surfaceId'); + } + return _surfaces.putIfAbsent( + surfaceId, + () => ValueNotifier(null), + ); + } + + /// Updates the definition of a surface. + /// + /// If [isNew] is true, a [SurfaceAdded] event is emitted. Otherwise, a + /// [SurfaceUpdated] event is emitted. + void updateSurface( + String surfaceId, + SurfaceDefinition definition, { + bool isNew = false, + }) { + final ValueNotifier notifier = _surfaces.putIfAbsent( + surfaceId, + () => ValueNotifier(null), + ); + notifier.value = definition; + + _surfaceOrder.remove(surfaceId); + _surfaceOrder.add(surfaceId); + + if (isNew) { + genUiLogger.info('Created new surface $surfaceId'); + _eventController.add(SurfaceAdded(surfaceId, definition)); + } else { + // genUiLogger.info('Updated surface $surfaceId'); // Optional logging + _eventController.add(SurfaceUpdated(surfaceId, definition)); + } + } + + /// Removes a surface from the registry. + /// + /// Emits a [SurfaceRemoved] event if the surface existed. + void removeSurface(String surfaceId) { + if (_surfaces.containsKey(surfaceId)) { + genUiLogger.info('Deleting surface $surfaceId'); + final ValueNotifier? notifier = _surfaces.remove( + surfaceId, + ); + notifier?.dispose(); + _surfaceOrder.remove(surfaceId); + _eventController.add(SurfaceRemoved(surfaceId)); + } + } + + /// Returns true if the registry contains a surface with the given + /// [surfaceId]. + bool hasSurface(String surfaceId) => _surfaces.containsKey(surfaceId); + + /// Returns the current definition of the surface with the given [surfaceId], + /// or null if it doesn't exist. + SurfaceDefinition? getSurface(String surfaceId) => + _surfaces[surfaceId]?.value; + + /// Disposes of the registry and all its resources. + void dispose() { + _eventController.close(); + for (final ValueNotifier notifier in _surfaces.values) { + notifier.dispose(); + } + } +} diff --git a/packages/genui/lib/src/facade/conversation.dart b/packages/genui/lib/src/facade/conversation.dart new file mode 100644 index 000000000..a16c55979 --- /dev/null +++ b/packages/genui/lib/src/facade/conversation.dart @@ -0,0 +1,208 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../engine/surface_controller.dart' show SurfaceController; +import '../engine/surface_controller.dart'; +import '../interfaces/transport.dart'; +import '../model/chat_message.dart'; +import '../model/ui_models.dart'; + +/// Events emitted by [Conversation] to notify listeners of changes. +sealed class ConversationEvent {} + +/// Fired when a new surface is added. +final class ConversationSurfaceAdded extends ConversationEvent { + /// Creates a [ConversationSurfaceAdded] event. + ConversationSurfaceAdded(this.surfaceId, this.definition); + + /// The ID of the added surface. + final String surfaceId; + + /// The definition of the added surface. + final SurfaceDefinition definition; +} + +/// Fired when components are updated on a surface. +final class ConversationComponentsUpdated extends ConversationEvent { + /// Creates a [ConversationComponentsUpdated] event. + ConversationComponentsUpdated(this.surfaceId, this.definition); + + /// The ID of the updated surface. + final String surfaceId; + + /// The new definition of the surface. + final SurfaceDefinition definition; +} + +/// Fired when a surface is removed. +final class ConversationSurfaceRemoved extends ConversationEvent { + /// Creates a [ConversationSurfaceRemoved] event. + ConversationSurfaceRemoved(this.surfaceId); + + /// The ID of the removed surface. + final String surfaceId; +} + +/// Fired when new content (text) is received from the LLM. +final class ConversationContentReceived extends ConversationEvent { + /// Creates a [ConversationContentReceived] event. + ConversationContentReceived(this.text); + + /// The text content received. + final String text; +} + +/// Fired when the conversation is waiting for a response. +final class ConversationWaiting extends ConversationEvent {} + +/// Fired when an error occurs during the conversation. +final class ConversationError extends ConversationEvent { + /// Creates a [ConversationError] event. + ConversationError(this.error, [this.stackTrace]); + + /// The error that occurred. + final Object error; + + /// The stack trace associated with the error, if any. + final StackTrace? stackTrace; +} + +/// State of the conversation. +class ConversationState { + /// Creates a [ConversationState]. + const ConversationState({ + required this.surfaces, + required this.latestText, + required this.isWaiting, + }); + + /// The list of active surface IDs. + final List surfaces; // Could be richer if needed + + /// The latest text received. + final String latestText; + + /// Whether we are waiting for a response. + final bool isWaiting; + + /// Creates a copy of this state with the given fields replaced. + ConversationState copyWith({ + List? surfaces, + String? latestText, + bool? isWaiting, + }) { + return ConversationState( + surfaces: surfaces ?? this.surfaces, + latestText: latestText ?? this.latestText, + isWaiting: isWaiting ?? this.isWaiting, + ); + } +} + +/// Facade for managing a GenUI conversation. +/// +/// Orchestrates compmunication between the [SurfaceController] and the +/// [Transport]. Manages the conversation state, including active surfaces, the +/// latest text response, and waiting status. +interface class Conversation { + /// Creates a [Conversation]. + /// + /// The [controller] manages the state of the UI surfaces. + /// The [transport] handles sending and receiving messages. + Conversation({required this.controller, required this.transport}) { + _transportSubscription = transport.incomingMessages.listen( + controller.handleMessage, + ); + + // Listen to transport text and emit events + _textSubscription = transport.incomingText.listen((text) { + _eventController.add(ConversationContentReceived(text)); + _updateState((s) => s.copyWith(latestText: text)); + }); + + // Listen to controller updates and emit events + _engineSubscription = controller.surfaceUpdates.listen((update) { + switch (update) { + case SurfaceAdded(:final surfaceId, :final definition): + _eventController.add(ConversationSurfaceAdded(surfaceId, definition)); + _updateState((s) { + if (!s.surfaces.contains(surfaceId)) { + return s.copyWith(surfaces: [...s.surfaces, surfaceId]); + } + return s; + }); + case ComponentsUpdated(:final surfaceId, :final definition): + _eventController.add( + ConversationComponentsUpdated(surfaceId, definition), + ); + case SurfaceRemoved(:final surfaceId): + _eventController.add(ConversationSurfaceRemoved(surfaceId)); + _updateState( + (s) => s.copyWith( + surfaces: s.surfaces.where((id) => id != surfaceId).toList(), + ), + ); + } + }); + + // Listen for controller submissions (e.g. errors or user actions) and + // forward them. + _engineSubmitSubscription = controller.onSubmit.listen(sendRequest); + } + + /// The controller that manages the surfaces. + final SurfaceController controller; + + /// The transport layer for sending and receiving messages. + final Transport transport; + + final StreamController _eventController = + StreamController.broadcast(); + + final ValueNotifier _stateNotifier = ValueNotifier( + const ConversationState(surfaces: [], latestText: '', isWaiting: false), + ); + + StreamSubscription? _transportSubscription; + StreamSubscription? _textSubscription; + StreamSubscription? _engineSubscription; + StreamSubscription? _engineSubmitSubscription; + + /// A stream of events emitted by the conversation. + Stream get events => _eventController.stream; + + /// The current state of the conversation. + ValueListenable get state => _stateNotifier; + + /// Sends a request to the LLM. + Future sendRequest(ChatMessage message) async { + _eventController.add(ConversationWaiting()); + _updateState((s) => s.copyWith(isWaiting: true)); + try { + await transport.sendRequest(message); + } catch (exception, stackTrace) { + _eventController.add(ConversationError(exception, stackTrace)); + } finally { + _updateState((s) => s.copyWith(isWaiting: false)); + } + } + + void _updateState(ConversationState Function(ConversationState) updater) { + _stateNotifier.value = updater(_stateNotifier.value); + } + + /// Disposes of the conversation and releases resources. + void dispose() { + _transportSubscription?.cancel(); + _textSubscription?.cancel(); + _engineSubscription?.cancel(); + _engineSubmitSubscription?.cancel(); + _eventController.close(); + _stateNotifier.dispose(); + } +} diff --git a/packages/genui/lib/src/facade/direct_call_integration/README.md b/packages/genui/lib/src/facade/direct_call_integration/README.md deleted file mode 100644 index 054d41627..000000000 --- a/packages/genui/lib/src/facade/direct_call_integration/README.md +++ /dev/null @@ -1 +0,0 @@ -Code that helps to use this library in direct call to REST Gemini API. diff --git a/packages/genui/lib/src/facade/direct_call_integration/model.dart b/packages/genui/lib/src/facade/direct_call_integration/model.dart deleted file mode 100644 index e6c671d87..000000000 --- a/packages/genui/lib/src/facade/direct_call_integration/model.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import '../../model/a2ui_message.dart'; - -/// A sealed class representing a part of a tool call. -sealed class Part { - /// Creates a [Part]. - const Part(); - - /// Creates a [Part] from a JSON map. - factory Part.fromJson(Map json) { - switch (json['type'] as String) { - case 'ToolCall': - return ToolCall.fromJson(json); - - default: - throw ArgumentError('Invalid Part type: ${json["type"]}'); - } - } - - /// Converts this object to a JSON representation. - Map toJson(); -} - -/// A tool call part. -class ToolCall extends Part { - /// The arguments to the tool call. - final Object? args; - - /// The name of the tool to call. - final String name; - - /// Creates a [ToolCall]. - const ToolCall({required this.args, required this.name}); - - /// Creates a [ToolCall] from a JSON map. - factory ToolCall.fromJson(Map json) => - ToolCall(args: json['args'], name: json['name'] as String); - - @override - Map toJson() => { - 'type': 'ToolCall', - 'args': args, - 'name': name, - }; -} - -/// Declaration to be provided to the LLM about a function/tool. -class GenUiFunctionDeclaration { - /// The description of the function. - final String description; - - /// The name of the function. - final String name; - - /// The parameters of the function. - final Object? parameters; - - /// Creates a [GenUiFunctionDeclaration]. - GenUiFunctionDeclaration({ - required this.description, - required this.name, - this.parameters, - }); - - /// Creates a [GenUiFunctionDeclaration] from a JSON map. - factory GenUiFunctionDeclaration.fromJson(Map json) => - GenUiFunctionDeclaration( - description: json['description'] as String, - name: json['name'] as String, - parameters: json['parameters'], - ); - - /// Converts this object to a JSON representation. - Map toJson() => { - 'description': description, - 'name': name, - 'parameters': parameters, - }; -} - -/// A parsed tool call. -class ParsedToolCall { - /// The A2UI messages from the tool call. - final List messages; - - /// The surface ID from the tool call. - final String surfaceId; - - /// Creates a [ParsedToolCall]. - ParsedToolCall({required this.messages, required this.surfaceId}); -} diff --git a/packages/genui/lib/src/facade/direct_call_integration/utils.dart b/packages/genui/lib/src/facade/direct_call_integration/utils.dart deleted file mode 100644 index 5a5f979c9..000000000 --- a/packages/genui/lib/src/facade/direct_call_integration/utils.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import '../../model/a2ui_message.dart'; -import '../../model/a2ui_schemas.dart'; -import '../../model/catalog.dart'; -import '../../model/tools.dart'; -import '../../primitives/simple_items.dart'; -import 'model.dart'; - -/// Prompt to be provided to the LLM about how to use the UI generation tools. -String genUiTechPrompt(List toolNames) { - final toolDescription = toolNames.length > 1 - ? 'the following UI generation tools: ' - '${toolNames.map((name) => '"$name"').join(', ')}' - : 'the UI generation tool "${toolNames.first}"'; - - return ''' -To show generated UI, use $toolDescription. -When generating UI, always provide a unique $surfaceIdKey to identify the UI surface: - -* To create new UI, use a new $surfaceIdKey. -* To update existing UI, use the existing $surfaceIdKey. - -Use the root component id: 'root'. -Ensure one of the generated components has an id of 'root'. -'''; -} - -/// Converts a [Catalog] to a [GenUiFunctionDeclaration]. -GenUiFunctionDeclaration catalogToFunctionDeclaration( - Catalog catalog, - String toolName, - String toolDescription, -) { - return GenUiFunctionDeclaration( - description: toolDescription, - name: toolName, - parameters: A2uiSchemas.surfaceUpdateSchema(catalog), - ); -} - -/// Parses a [ToolCall] into a [ParsedToolCall]. -ParsedToolCall parseToolCall(ToolCall toolCall, String toolName) { - assert(toolCall.name == toolName); - - final Map messageJson = {'surfaceUpdate': toolCall.args}; - final surfaceUpdateMessage = A2uiMessage.fromJson(messageJson); - - final surfaceId = (toolCall.args as JsonMap)[surfaceIdKey] as String; - - final beginRenderingMessage = BeginRendering( - surfaceId: surfaceId, - root: 'root', - ); - - return ParsedToolCall( - messages: [surfaceUpdateMessage, beginRenderingMessage], - surfaceId: surfaceId, - ); -} - -/// Converts a catalog example to a [ToolCall]. -ToolCall catalogExampleToToolCall( - JsonMap example, - String toolName, - String surfaceId, -) { - final messageJson = {'surfaceUpdate': example}; - final surfaceUpdateMessage = A2uiMessage.fromJson(messageJson); - - return ToolCall( - name: toolName, - args: {surfaceIdKey: surfaceId, 'surfaceUpdate': surfaceUpdateMessage}, - ); -} diff --git a/packages/genui/lib/src/facade/gen_ui_conversation.dart b/packages/genui/lib/src/facade/gen_ui_conversation.dart deleted file mode 100644 index 466dd1c19..000000000 --- a/packages/genui/lib/src/facade/gen_ui_conversation.dart +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import '../content_generator.dart'; -import '../core/a2ui_message_processor.dart'; -import '../model/a2ui_client_capabilities.dart'; -import '../model/a2ui_message.dart'; -import '../model/chat_message.dart'; -import '../model/ui_models.dart'; - -/// A high-level abstraction to manage a generative UI conversation. -/// -/// This class simplifies the process of creating a generative UI by managing -/// the conversation loop and the interaction with the AI. It encapsulates a -/// `A2uiMessageProcessor` and a `ContentGenerator`, providing a single entry -/// point for sending user requests and receiving UI updates. -/// -/// This is a convenience facade for the specific use case of a linear -/// conversation that can contain Gen UI surfaces. -class GenUiConversation { - /// Creates a new [GenUiConversation]. - /// - /// Callbacks like [onSurfaceAdded], [onSurfaceUpdated] and [onSurfaceDeleted] - /// can be provided to react to UI changes initiated by the AI. - GenUiConversation({ - this.onSurfaceAdded, - this.onSurfaceUpdated, - this.onSurfaceDeleted, - this.onTextResponse, - this.onError, - required this.contentGenerator, - required this.a2uiMessageProcessor, - }) { - _a2uiSubscription = contentGenerator.a2uiMessageStream.listen( - a2uiMessageProcessor.handleMessage, - ); - _userEventSubscription = a2uiMessageProcessor.onSubmit.listen(sendRequest); - _surfaceUpdateSubscription = a2uiMessageProcessor.surfaceUpdates.listen( - _handleSurfaceUpdate, - ); - _textResponseSubscription = contentGenerator.textResponseStream.listen( - _handleTextResponse, - ); - _errorSubscription = contentGenerator.errorStream.listen(_handleError); - } - - /// The [ContentGenerator] for the conversation. - final ContentGenerator contentGenerator; - - /// The manager for the UI surfaces in the conversation. - final A2uiMessageProcessor a2uiMessageProcessor; - - /// A callback for when a new surface is added by the AI. - final ValueChanged? onSurfaceAdded; - - /// A callback for when a surface is deleted by the AI. - final ValueChanged? onSurfaceDeleted; - - /// A callback for when a surface is updated by the AI. - final ValueChanged? onSurfaceUpdated; - - /// A callback for when a text response is received from the AI. - final ValueChanged? onTextResponse; - - /// A callback for when an error occurs in the content generator. - final ValueChanged? onError; - - late final StreamSubscription _a2uiSubscription; - late final StreamSubscription _userEventSubscription; - late final StreamSubscription _surfaceUpdateSubscription; - late final StreamSubscription _textResponseSubscription; - late final StreamSubscription _errorSubscription; - - final ValueNotifier> _conversation = - ValueNotifier>([]); - - void _handleSurfaceUpdate(GenUiUpdate update) { - switch (update) { - case SurfaceAdded(): - _conversation.value = [ - ..._conversation.value, - AiUiMessage( - definition: update.definition, - surfaceId: update.surfaceId, - ), - ]; - onSurfaceAdded?.call(update); - case SurfaceUpdated(): - final newConversation = List.from(_conversation.value); - final int index = newConversation.lastIndexWhere( - (m) => m is AiUiMessage && m.surfaceId == update.surfaceId, - ); - final newMessage = AiUiMessage( - definition: update.definition, - surfaceId: update.surfaceId, - ); - if (index != -1) { - newConversation[index] = newMessage; - } else { - // This can happen if a surface is created and updated in the same - // turn. - newConversation.add(newMessage); - } - _conversation.value = newConversation; - onSurfaceUpdated?.call(update); - case SurfaceRemoved(): - final newConversation = List.from(_conversation.value); - newConversation.removeWhere( - (m) => m is AiUiMessage && m.surfaceId == update.surfaceId, - ); - _conversation.value = newConversation; - onSurfaceDeleted?.call(update); - } - } - - /// Disposes of the resources used by this agent. - void dispose() { - _a2uiSubscription.cancel(); - _userEventSubscription.cancel(); - _surfaceUpdateSubscription.cancel(); - _textResponseSubscription.cancel(); - _errorSubscription.cancel(); - contentGenerator.dispose(); - a2uiMessageProcessor.dispose(); - } - - /// The host for the UI surfaces managed by this agent. - GenUiHost get host => a2uiMessageProcessor; - - /// A [ValueListenable] that provides the current conversation history. - ValueListenable> get conversation => _conversation; - - /// A [ValueListenable] that indicates whether the agent is currently - /// processing a request. - ValueListenable get isProcessing => contentGenerator.isProcessing; - - /// Returns a [ValueNotifier] for the given [surfaceId]. - ValueNotifier surface(String surfaceId) { - return a2uiMessageProcessor.getSurfaceNotifier(surfaceId); - } - - /// Sends a user message to the AI to generate a UI response. - Future sendRequest(ChatMessage message) async { - final List history = _conversation.value; - if (message is! UserUiInteractionMessage) { - _conversation.value = [...history, message]; - } - final clientCapabilities = A2UiClientCapabilities( - supportedCatalogIds: a2uiMessageProcessor.catalogs - .map((c) => c.catalogId) - .where((id) => id != null) - .cast() - .toList(), - ); - return contentGenerator.sendRequest( - message, - history: history, - clientCapabilities: clientCapabilities, - ); - } - - void _handleTextResponse(String text) { - _conversation.value = [..._conversation.value, AiTextMessage.text(text)]; - onTextResponse?.call(text); - } - - void _handleError(ContentGeneratorError error) { - // Add an error representation to the conversation history so the AI can see - // that something failed. - final errorResponseMessage = AiTextMessage.text( - 'An error occurred: ${error.error}', - ); - _conversation.value = [..._conversation.value, errorResponseMessage]; - onError?.call(error); - } -} diff --git a/packages/genui/lib/src/facade/prompt_builder.dart b/packages/genui/lib/src/facade/prompt_builder.dart new file mode 100644 index 000000000..dfe2a2c28 --- /dev/null +++ b/packages/genui/lib/src/facade/prompt_builder.dart @@ -0,0 +1,65 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../genui.dart'; + +// TODO(polina-c): add allowed surface operations +// TODO(polina-c): consider incorporating catalog rules to the catalog + +/// A builder for a prompt to generate UI. +class PromptBuilder { + /// Creates a chat prompt builder. + /// + /// The builder will generate a prompt for a chat session, + /// that instructs to create new surfaces for each response + /// and restrict surface deletion and updates. + PromptBuilder.chat({required this.catalog, this.instructions}); + + /// Instructions for the generated UI. + /// + /// This can include description of target user profile, + /// description of the typical tasks the user wants to perform, + /// wanted profile of the AI agent, examples of good responses, + /// explanation when to use which catalog items. + final String? instructions; + + /// Catalog to use for the generated UI. + final Catalog catalog; + + late final String systemPrompt = () { + final String a2uiSchema = A2uiMessage.a2uiMessageSchema( + catalog, + ).toJson(indent: ' '); + + return ''' +${instructions ?? ''} + +IMPORTANT: When you generate UI in a response, you MUST always create +a new surface with a unique `surfaceId`. Do NOT reuse or update +existing `surfaceId`s. Each UI response must be in its own new surface. + +Do not delete existing surfaces. + + +$a2uiSchema + + +${BasicCatalogEmbed.basicCatalogRules} + +$_basicChatPromptFragment'''; + }(); +} + +/// A basic chat prompt fragment. +const String _basicChatPromptFragment = ''' + +# Outputting UI information + +Use the provided tools to respond to the user using rich UI elements. + +Important considerations: +- When you are asking for information from the user, you should always include + at least one submit button of some kind or another submitting element so that + the user can indicate that they are done providing information. +'''; diff --git a/packages/genui/lib/src/functions/expression_parser.dart b/packages/genui/lib/src/functions/expression_parser.dart new file mode 100644 index 000000000..0c4fba7eb --- /dev/null +++ b/packages/genui/lib/src/functions/expression_parser.dart @@ -0,0 +1,461 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../model/data_model.dart'; +import '../primitives/logging.dart'; +import '../primitives/simple_items.dart'; +import 'functions.dart'; + +/// Parses and evaluates expressions in the A2UI `${expression}` format. +class ExpressionParser { + ExpressionParser(this.context); + + final DataContext context; + final FunctionRegistry _functions = FunctionRegistry(); + + static const int _maxRecursionDepth = 100; + + /// Parses the input string and resolves any embedded expressions. + /// + /// If the string contains a single expression that encompasses the entire + /// string (e.g. "${/foo}"), the return value may be of any type (not just + /// [String]). + /// + /// If the string contains text mixed with expressions (e.g. "Value: ${/foo}"), + /// the return value will always be a [String]. + /// + /// This method is the entry point for expression resolution. It handles + /// escaping of the `${` sequence using a backslash (e.g. `\${`). + /// Parses the input string and resolves any embedded expressions. + /// + /// If the string contains a single expression that encompasses the entire + /// string (e.g. "${/foo}"), the return value may be of any type (not just + /// [String]). + /// + /// If the string contains text mixed with expressions (e.g. "Value: ${/foo}"), + /// the return value will always be a [String]. + /// + /// This method is the entry point for expression resolution. It handles + /// escaping of the `${` sequence using a backslash (e.g. `\${`). + Object? parse(String input) { + if (!input.contains(r'${')) { + return input; + } + return _parseStringWithInterpolations(input, null); + } + + /// Evaluates an expression which can be a String, Map (function call/path), etc. + Object? evaluate(Object? expression) { + if (expression is String) { + return parse(expression); + } + if (expression is Map) { + if (expression.containsKey('call')) { + return evaluateFunctionCall(expression as JsonMap); + } + if (expression.containsKey('path')) { + return _resolvePath(expression['path'] as String, null); + } + } + return expression; + } + + /// Extracts all data paths referenced in the given input. + /// + /// This method parses the input without evaluating functions, collecting + /// all paths that would be accessed during evaluation. + Set extractDependencies(String input) { + if (!input.contains(r'${')) { + return {}; + } + final Set dependencies = {}; + _parseStringWithInterpolations(input, dependencies); + return dependencies; + } + + /// Extracts all data paths referenced in the given expression (String or + /// Map). + Set extractDependenciesFrom(Object? expression) { + final Set dependencies = {}; + _extractDependenciesFrom(expression, dependencies); + return dependencies; + } + + void _extractDependenciesFrom( + Object? expression, + Set dependencies, + ) { + if (expression is String) { + if (expression.contains(r'${')) { + _parseStringWithInterpolations(expression, dependencies); + } + } else if (expression is Map) { + if (expression.containsKey('path')) { + dependencies.add( + context.resolvePath(DataPath(expression['path'] as String)), + ); + } else if (expression.containsKey('call')) { + evaluateFunctionCall(expression as JsonMap, dependencies: dependencies); + } else { + // Recursively check values for other map types if necessary? + // Usually expressions are structured strictly. + // But functions args are maps. + for (final Object? value in expression.values) { + _extractDependenciesFrom(value, dependencies); + } + } + } else if (expression is List) { + for (final Object? item in expression) { + _extractDependenciesFrom(item, dependencies); + } + } + } + + /// Evaluates a dynamic boolean condition. + /// + /// The [condition] can be: + /// - [bool]: Returns the boolean value directly. + /// - [Map]: + /// - If it has a 'call' key, it is evaluated as a function call. + /// - If it has a 'path' key, it is evaluated as a data binding. + /// - [String]: Parsed as an expression, then checked for truthiness. + bool evaluateCondition(Object? condition) { + if (condition == null) return false; + if (condition is bool) return condition; + + Object? result; + if (condition is String) { + result = parse(condition); + } else if (condition is Map) { + if (condition.containsKey('call')) { + result = evaluateFunctionCall(condition as JsonMap); + } else if (condition.containsKey('path')) { + result = _resolvePath(condition['path'] as String, null); + } else { + // Unknown map format, return false safely. + return false; + } + } else { + result = condition; + } + + if (result is bool) return result; + return result != null; + } + + /// Evaluates a function call defined in [callDefinition]. + /// + /// The [callDefinition] must contain a 'call' key with the function name + /// and an optional 'args' key with a map of arguments. + Object? evaluateFunctionCall( + JsonMap callDefinition, { + Set? dependencies, + }) { + final name = callDefinition['call'] as String?; + if (name == null) { + // Not a function call or missing 'call' property. + return null; + } + + final Map args = {}; + final Object? argsJson = callDefinition['args']; + + if (argsJson is Map) { + for (final Object? key in argsJson.keys) { + final argName = key.toString(); + final Object? value = argsJson[key]; + if (value is String) { + args[argName] = _parseStringWithInterpolations(value, dependencies); + } else if (value is Map && value.containsKey('path')) { + args[argName] = _resolvePath(value['path'] as String, dependencies); + } else if (value is Map && value.containsKey('call')) { + // Recursive evaluation for nested calls + args[argName] = evaluateFunctionCall( + value as JsonMap, + dependencies: dependencies, + ); + } else { + args[argName] = value; + } + } + } else if (argsJson != null) { + genUiLogger.warning( + 'Function $name called with invalid args type: ' + '${argsJson.runtimeType}. Expected Map. Arguments dropped.', + ); + } + + if (dependencies != null) { + return null; // Don't execute function if collecting dependencies + } + + return _functions.invoke(name, args); + } + + Object? _parseStringWithInterpolations( + String input, + Set? dependencies, + ) { + var i = 0; + + final parts = []; + + while (i < input.length) { + final int startIndex = input.indexOf(r'${', i); + if (startIndex == -1) { + parts.add(input.substring(i)); + break; + } + + if (startIndex > 0 && input[startIndex - 1] == r'\') { + parts.add(input.substring(i, startIndex - 1)); + parts.add(r'${'); + i = startIndex + 2; + continue; + } + + if (startIndex > i) { + parts.add(input.substring(i, startIndex)); + } + final (String content, int endIndex) = _extractExpressionContent( + input, + startIndex + 2, + ); + if (endIndex == -1) { + parts.add(input.substring(startIndex)); + break; + } + + final Object? value = _evaluateExpression(content, 0, dependencies); + parts.add(value); + + i = endIndex + 1; // Skip closing '}' + } + + if (parts.length == 1 && parts[0] is! String) { + return parts[0]; + } + + if (dependencies != null) { + return null; + } + + return parts.map((e) => e?.toString() ?? '').join(''); + } + + (String, int) _extractExpressionContent(String input, int start) { + var balance = 1; + var i = start; + while (i < input.length) { + if (input[i] == r'{') { + balance++; + } else if (input[i] == r'}') { + balance--; + if (balance == 0) { + return (input.substring(start, i), i); + } + } + if (input[i] == "'" || input[i] == '"') { + final String quote = input[i]; + i++; + while (i < input.length) { + if (input[i] == quote && input[i - 1] != r'\') { + break; + } + i++; + } + } + i++; + } + return ('', -1); + } + + Object? _evaluateExpression( + String content, + int depth, + Set? dependencies, + ) { + if (depth > _maxRecursionDepth) { + genUiLogger.warning( + 'Max recursion depth reached in expression: $content', + ); + return null; + } + + content = content.trim(); + + final RegExpMatch? match = RegExp( + r'^([a-zA-Z0-9_]+)\s*\(', + ).firstMatch(content); + if (match != null && content.endsWith(')')) { + final String funcName = match.group(1)!; + final String argsStr = content.substring(match.end, content.length - 1); + final Map args = _parseNamedArgs( + argsStr, + depth + 1, + dependencies, + ); + + if (dependencies != null) { + return null; + } + return _functions.invoke(funcName, args); + } + + return _resolvePath(content, dependencies); + } + + Map _parseNamedArgs( + String argsStr, + int depth, + Set? dependencies, + ) { + final args = {}; + var i = 0; + + while (i < argsStr.length) { + // Skip whitespace + while (i < argsStr.length && argsStr[i].trim().isEmpty) { + i++; + } + if (i >= argsStr.length) break; + + // Expect key + final keyStart = i; + while (i < argsStr.length && + argsStr[i] != ':' && + argsStr[i] != ' ' && + argsStr[i] != ',') { + i++; + } + final String key = argsStr.substring(keyStart, i).trim(); + + // Skip whitespace after key + while (i < argsStr.length && argsStr[i].trim().isEmpty) { + i++; + } + + // Expect colon + if (i < argsStr.length && argsStr[i] == ':') { + i++; // skip colon + } else { + genUiLogger.warning( + 'Invalid named argument format (missing colon) at index $i: $argsStr', + ); + return args; + } + + // Skip whitespace after colon + while (i < argsStr.length && argsStr[i].trim().isEmpty) { + i++; + } + + // Parse Value + final (Object? value, int nextIndex) = _parseValue( + argsStr, + i, + depth, + dependencies, + ); + args[key] = value; + i = nextIndex; + + // Skip whitespace after value + while (i < argsStr.length && argsStr[i].trim().isEmpty) { + i++; + } + + // Expect comma or end + if (i < argsStr.length && argsStr[i] == ',') { + i++; + } + } + return args; + } + + (Object?, int) _parseValue( + String input, + int start, + int depth, + Set? dependencies, + ) { + if (start >= input.length) return (null, start); + + final String char = input[start]; + + // String literal + if (char == "'" || char == '"') { + final quote = char; + int i = start + 1; + while (i < input.length) { + if (input[i] == quote && input[i - 1] != r'\') { + break; + } + i++; + } + if (i < input.length) { + // Found closing quote + final String val = input.substring(start + 1, i); + // Recursively parse string for interpolations + return (_parseStringWithInterpolations(val, dependencies), i + 1); + } + return (input.substring(start), input.length); // Unclosed string + } + + // Expression ${...} + if (char == r'$' && start + 1 < input.length && input[start + 1] == '{') { + final (String content, int end) = _extractExpressionContent( + input, + start + 2, + ); + if (end != -1) { + final Object? val = _evaluateExpression(content, depth, dependencies); + return (val, end + 1); + } + } + + // Heuristic for function calls REMOVED. + // Arguments must be Literals (quoted strings, booleans, numbers) + // or Nested Expressions (${...}). + + // Number / Boolean / Null / Path + // Read the next token, stopping at delimiters like comma, parenthesis, or + // brace. This allows `_parseNamedArgs` to handle the delimiters + // appropriately. + + var i = start; + while (i < input.length) { + final String c = input[i]; + if (c == ',' || + c == ')' || + c == '}' || + c == ' ' || + c == '\t' || + c == '\n') { + break; + } + i++; + } + + final String token = input.substring(start, i); + if (token == 'true') return (true, i); + if (token == 'false') return (false, i); + if (token == 'null') return (null, i); + + final num? numVal = num.tryParse(token); + if (numVal != null) return (numVal, i); + + // Treat as a DataPath if no other type matches. + return (_resolvePath(token, dependencies), i); + } + + Object? _resolvePath(String pathStr, Set? dependencies) { + pathStr = pathStr.trim(); + if (dependencies != null) { + dependencies.add(context.resolvePath(DataPath(pathStr))); + return null; + } + return context.getValue(pathStr); + } +} diff --git a/packages/genui/lib/src/functions/functions.dart b/packages/genui/lib/src/functions/functions.dart new file mode 100644 index 000000000..8b94688ce --- /dev/null +++ b/packages/genui/lib/src/functions/functions.dart @@ -0,0 +1,269 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../primitives/logging.dart'; + +/// A function that can be called from the UI definition. +typedef ClientFunction = Object? Function(Map args); + +/// Registry of available client-side functions. +class FunctionRegistry { + static final FunctionRegistry _instance = FunctionRegistry._init(); + + factory FunctionRegistry() => _instance; + + FunctionRegistry._init() { + registerStandardFunctions(); + } + + final Map _functions = {}; + + /// Registers a function with the given [name]. + void register(String name, ClientFunction function) { + _functions[name] = function; + } + + /// Invokes a registered function with [name] and [args]. + Object? invoke(String name, Map args) { + final ClientFunction? func = _functions[name]; + if (func == null) { + genUiLogger.warning('Function not found: $name'); + return null; + } + try { + return func(args); + } catch (exception, stackTrace) { + throw FunctionInvocationException(name, exception, stackTrace); + } + } + + /// Registers all standard A2UI functions. + void registerStandardFunctions() { + register('required', _required); + register('regex', _regex); + register('length', _length); + register('numeric', _numeric); + register('email', _email); + register('formatString', _formatString); + register('openUrl', _openUrl); + register('formatNumber', _formatNumber); + register('formatCurrency', _formatCurrency); + register('formatDate', _formatDate); + register('pluralize', _pluralize); + register('and', _and); + register('or', _or); + register('not', _not); + } + + // --- Implementations --- + + Object? _and(Map args) { + if (!args.containsKey('values')) return false; + final Object? values = args['values']; + if (values is! List) return false; + // We assume the caller (parser) has evaluated the list items if they were + // expressions, but if the list contains plain boolean values or + // truthy/falsy values, we check them. + for (final Object? element in values) { + if (!_isTruthy(element)) return false; + } + return true; + } + + Object? _or(Map args) { + if (!args.containsKey('values')) return false; + final Object? values = args['values']; + if (values is! List) return false; + for (final Object? element in values) { + if (_isTruthy(element)) return true; + } + return false; + } + + Object? _not(Map args) { + if (!args.containsKey('value')) return false; + return !_isTruthy(args['value']); + } + + bool _isTruthy(Object? value) { + if (value is bool) return value; + if (value == null) return false; + // You might want to define other truthy rules here + return true; + } + + Object? _required(Map args) { + if (!args.containsKey('value')) return false; + final Object? value = args['value']; + if (value == null) return false; + if (value is String) return value.isNotEmpty; + if (value is List) return value.isNotEmpty; + if (value is Map) return value.isNotEmpty; + return true; + } + + Object? _regex(Map args) { + final Object? value = args['value']; + final Object? pattern = args['pattern']; + if (value is! String || pattern is! String) return false; + try { + return RegExp(pattern).hasMatch(value); + } on FormatException catch (exception, stackTrace) { + throw FunctionInvocationException( + 'regex', + 'Invalid regex pattern: $pattern. $exception', + stackTrace, + ); + } + } + + Object? _length(Map args) { + final Object? value = args['value']; + if (value == null) return false; + + int? len; + if (value is String) { + len = value.length; + } else if (value is List) { + len = value.length; + } else { + return false; + } + + if (args.containsKey('min')) { + final Object? min = args['min']; + if (min is num && len < min) return false; + } + if (args.containsKey('max')) { + final Object? max = args['max']; + if (max is num && len > max) return false; + } + return true; + } + + Object? _numeric(Map args) { + final Object? value = args['value']; + if (value is! num) return false; + + if (args.containsKey('min')) { + final Object? min = args['min']; + if (min is num && value < min) return false; + } + if (args.containsKey('max')) { + final Object? max = args['max']; + if (max is num && value > max) return false; + } + return true; + } + + Object? _email(Map args) { + final Object? value = args['value']; + if (value is! String) return false; + final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$'); + return emailRegex.hasMatch(value); + } + + Object? _formatString(Map args) { + return args['value']?.toString() ?? ''; + } + + Object? _openUrl(Map args) async { + final Object? urlStr = args['url']; + if (urlStr is! String) return false; + final Uri? uri = Uri.tryParse(urlStr); + if (uri != null) { + if (await canLaunchUrl(uri)) { + return await launchUrl(uri); + } + } + return false; + } + + Object? _formatNumber(Map args) { + final Object? number = args['value']; + if (number is! num) return number?.toString() ?? ''; + + int? decimalPlaces; + if (args['decimalPlaces'] is num) { + decimalPlaces = (args['decimalPlaces'] as num).toInt(); + } + + var useGrouping = true; + if (args['useGrouping'] is bool) { + useGrouping = args['useGrouping'] as bool; + } + + final formatter = NumberFormat.decimalPattern(); // Default locale + if (!useGrouping) { + formatter.turnOffGrouping(); + } + if (decimalPlaces != null) { + formatter.minimumFractionDigits = decimalPlaces; + formatter.maximumFractionDigits = decimalPlaces; + } + + return formatter.format(number); + } + + Object? _formatCurrency(Map args) { + final Object? amount = args['value']; + final Object? currencyCode = args['currencyCode']; + if (amount is! num || currencyCode is! String) { + return amount?.toString() ?? ''; + } + + final formatter = NumberFormat.simpleCurrency(name: currencyCode); + return formatter.format(amount); + } + + Object? _formatDate(Map args) { + final Object? dateVal = args['value']; + final Object? pattern = args['pattern']; + + DateTime? date; + if (dateVal is String) { + date = DateTime.tryParse(dateVal); + } else if (dateVal is int) { + date = DateTime.fromMillisecondsSinceEpoch(dateVal); + } + + if (date == null || pattern is! String) return dateVal?.toString(); + + try { + return DateFormat(pattern).format(date); + } catch (e) { + return date.toString(); + } + } + + Object? _pluralize(Map args) { + final Object? count = args['count']; + if (count is! num) return ''; + + if (count == 0 && args.containsKey('zero')) return args['zero']; + if (count == 1 && args.containsKey('one')) return args['one']; + return args['other'] ?? ''; + } +} + +/// Exception thrown when a function invocation fails. +class FunctionInvocationException implements Exception { + /// Creates a [FunctionInvocationException]. + FunctionInvocationException(this.functionName, this.cause, [this.stack]); + + /// The name of the function that failed. + final String functionName; + + /// The underlying cause of the failure. + final Object cause; + + /// The stack trace. + final StackTrace? stack; + + @override + String toString() => 'Error invoking function "$functionName": $cause'; +} diff --git a/packages/genui/lib/src/interfaces/a2ui_message_sink.dart b/packages/genui/lib/src/interfaces/a2ui_message_sink.dart new file mode 100644 index 000000000..869aec6c6 --- /dev/null +++ b/packages/genui/lib/src/interfaces/a2ui_message_sink.dart @@ -0,0 +1,11 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../model/a2ui_message.dart'; + +/// An interface for a message sink that accepts [A2uiMessage]s. +abstract interface class A2uiMessageSink { + /// Handles a message from the client. + void handleMessage(A2uiMessage message); +} diff --git a/packages/genui/lib/src/interfaces/surface_context.dart b/packages/genui/lib/src/interfaces/surface_context.dart new file mode 100644 index 000000000..2e87ccf03 --- /dev/null +++ b/packages/genui/lib/src/interfaces/surface_context.dart @@ -0,0 +1,32 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import '../model/catalog.dart'; +import '../model/data_model.dart'; +import '../model/ui_models.dart'; + +/// An interface for a specific UI surface context. +/// +/// This provides access to the state and definition of a single surface. +abstract interface class SurfaceContext { + /// The ID of the surface this context is bound to. + String get surfaceId; + + /// The current definition of the UI for this surface. + ValueListenable get definition; + + /// The data model for this surface. + DataModel get dataModel; + + /// The catalogs available to this surface. + Iterable get catalogs; + + /// Handles a UI event from this surface. + void handleUiEvent(UiEvent event); + + /// Reports an error capable of being sent back to the AI. + void reportError(Object error, StackTrace? stack); +} diff --git a/packages/genui/lib/src/interfaces/surface_host.dart b/packages/genui/lib/src/interfaces/surface_host.dart new file mode 100644 index 000000000..656316f6a --- /dev/null +++ b/packages/genui/lib/src/interfaces/surface_host.dart @@ -0,0 +1,32 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import '../../genui.dart' show SurfaceContext; + +import '../model/ui_models.dart'; +import 'surface_context.dart'; + +/// An interface for a host that manages UI surfaces. +/// +/// This host provides updates when surfaces are added, removed, or changed. +/// It also acts as a factory for [SurfaceContext]s, which provide access to the +/// state of minimal, individual surfaces. +abstract interface class SurfaceHost { + /// A stream of updates for the surfaces managed by this host. + /// + /// Implementations may choose to filter redundant updates. Consumers should + /// rely on [contextFor] to get the context for a specific surface. + Stream get surfaceUpdates; + + /// Returns a [ValueListenable] that tracks the definition of the surface + /// with the given [surfaceId]. + ValueListenable watchSurface(String surfaceId); + + /// Returns a [SurfaceContext] for the surface with the given [surfaceId]. + SurfaceContext contextFor(String surfaceId); +} diff --git a/packages/genui/lib/src/interfaces/transport.dart b/packages/genui/lib/src/interfaces/transport.dart new file mode 100644 index 000000000..ab53ffaed --- /dev/null +++ b/packages/genui/lib/src/interfaces/transport.dart @@ -0,0 +1,29 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../model/a2ui_message.dart'; +import '../model/chat_message.dart'; + +/// An interface for transporting messages between GenUI and an AI service. +/// +/// This unifies the concept of incoming streams (text chunks and A2UI messages) +/// and outgoing requests. +abstract interface class Transport { + /// A stream of raw text chunks received from the AI service. + /// + /// This is typically used for "streaming" responses where the text is built + /// up over time. + Stream get incomingText; + + /// A stream of parsed [A2uiMessage]s received from the AI service. + Stream get incomingMessages; + + /// Sends a request to the AI service. + Future sendRequest(ChatMessage message); + + /// Disposes of any resources used by this transport. + void dispose(); +} diff --git a/packages/genui/lib/src/model/a2ui_client_capabilities.dart b/packages/genui/lib/src/model/a2ui_client_capabilities.dart index 8ec6c5ed6..d5f47f954 100644 --- a/packages/genui/lib/src/model/a2ui_client_capabilities.dart +++ b/packages/genui/lib/src/model/a2ui_client_capabilities.dart @@ -18,8 +18,8 @@ class A2UiClientCapabilities { /// A list of identifiers for all pre-defined catalogs the client supports. /// - /// The client MUST always include the standard catalog ID here if it - /// supports it. + /// The client MUST always include the basic catalog ID here if it + /// supports said catalog. final List supportedCatalogIds; /// An array of full Catalog Definition Documents. @@ -35,6 +35,6 @@ class A2UiClientCapabilities { if (inlineCatalogs != null) { json['inlineCatalogs'] = inlineCatalogs; } - return json; + return {'v0.9': json}; } } diff --git a/packages/genui/lib/src/model/a2ui_message.dart b/packages/genui/lib/src/model/a2ui_message.dart index c026fc532..181d18c07 100644 --- a/packages/genui/lib/src/model/a2ui_message.dart +++ b/packages/genui/lib/src/model/a2ui_message.dart @@ -4,10 +4,11 @@ import 'package:json_schema_builder/json_schema_builder.dart'; +import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; import 'a2ui_schemas.dart'; import 'catalog.dart'; -import 'tools.dart'; +import 'data_model.dart'; import 'ui_models.dart'; /// A sealed class representing a message in the A2UI stream. @@ -15,140 +16,237 @@ sealed class A2uiMessage { /// Creates an [A2uiMessage]. const A2uiMessage(); + /// Creates an [A2uiMessage] from a JSON map. /// Creates an [A2uiMessage] from a JSON map. factory A2uiMessage.fromJson(JsonMap json) { - if (json.containsKey('surfaceUpdate')) { - return SurfaceUpdate.fromJson(json['surfaceUpdate'] as JsonMap); - } - if (json.containsKey('dataModelUpdate')) { - return DataModelUpdate.fromJson(json['dataModelUpdate'] as JsonMap); - } - if (json.containsKey('beginRendering')) { - return BeginRendering.fromJson(json['beginRendering'] as JsonMap); - } - if (json.containsKey('deleteSurface')) { - return SurfaceDeletion.fromJson(json['deleteSurface'] as JsonMap); + try { + final Object? version = json['version']; + if (version != 'v0.9' && version != '0.9') { + throw A2uiValidationException( + 'A2UI message must have version "v0.9" (or "0.9")', + json: json, + ); + } + if (json.containsKey('createSurface')) { + try { + return CreateSurface.fromJson(json['createSurface'] as JsonMap); + } catch (e) { + throw A2uiValidationException( + 'Failed to parse CreateSurface message', + json: json, + cause: e, + ); + } + } + if (json.containsKey('updateComponents')) { + try { + return UpdateComponents.fromJson(json['updateComponents'] as JsonMap); + } catch (e) { + throw A2uiValidationException( + 'Failed to parse UpdateComponents message', + json: json, + cause: e, + ); + } + } + if (json.containsKey('updateDataModel')) { + try { + return UpdateDataModel.fromJson(json['updateDataModel'] as JsonMap); + } catch (e) { + throw A2uiValidationException( + 'Failed to parse UpdateDataModel message', + json: json, + cause: e, + ); + } + } + if (json.containsKey('deleteSurface')) { + try { + return DeleteSurface.fromJson(json['deleteSurface'] as JsonMap); + } catch (e) { + throw A2uiValidationException( + 'Failed to parse DeleteSurface message', + json: json, + cause: e, + ); + } + } + } on A2uiValidationException { + rethrow; + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Failed to parse A2UI message from JSON: $json', + exception, + stackTrace, + ); + rethrow; } - throw ArgumentError('Unknown A2UI message type: $json'); + throw A2uiValidationException( + 'Unknown A2UI message type: ${json.keys}', + json: json, + ); } /// Returns the JSON schema for an A2UI message. static Schema a2uiMessageSchema(Catalog catalog) { - return S.object( - title: 'A2UI Message Schema', - description: - """Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.""", - properties: { - 'surfaceUpdate': A2uiSchemas.surfaceUpdateSchema(catalog), - 'dataModelUpdate': A2uiSchemas.dataModelUpdateSchema(), - 'beginRendering': A2uiSchemas.beginRenderingSchema(), - 'deleteSurface': A2uiSchemas.surfaceDeletionSchema(), - }, + return S.combined( + allOf: [ + S.object( + title: 'A2UI Message Schema', + description: + 'Describes a JSON payload for an A2UI (Agent to UI) message. ' + 'A message MUST contain exactly ONE of the action properties.', + properties: { + 'version': S.string(constValue: 'v0.9'), + 'createSurface': A2uiSchemas.createSurfaceSchema(), + 'updateComponents': A2uiSchemas.updateComponentsSchema(catalog), + 'updateDataModel': A2uiSchemas.updateDataModelSchema(), + 'deleteSurface': A2uiSchemas.deleteSurfaceSchema(), + }, + required: ['version'], + ), + ], + anyOf: [ + { + 'required': ['createSurface'], + }, + { + 'required': ['updateComponents'], + }, + { + 'required': ['updateDataModel'], + }, + { + 'required': ['deleteSurface'], + }, + ], ); } } -/// An A2UI message that updates a surface with new components. -final class SurfaceUpdate extends A2uiMessage { - /// Creates a [SurfaceUpdate] message. - const SurfaceUpdate({required this.surfaceId, required this.components}); +/// An A2UI message that signals the client to create and show a new surface. +final class CreateSurface extends A2uiMessage { + /// Creates a [CreateSurface] message. + const CreateSurface({ + required this.surfaceId, + required this.catalogId, + this.theme, + this.sendDataModel = false, + }); - /// Creates a [SurfaceUpdate] message from a JSON map. - factory SurfaceUpdate.fromJson(JsonMap json) { - return SurfaceUpdate( + /// Creates a [CreateSurface] message from a JSON map. + factory CreateSurface.fromJson(JsonMap json) { + return CreateSurface( surfaceId: json[surfaceIdKey] as String, - components: (json['components'] as List) - .map((e) => Component.fromJson(e as JsonMap)) - .toList(), + catalogId: json['catalogId'] as String, + theme: json['theme'] as JsonMap?, + sendDataModel: json['sendDataModel'] as bool? ?? false, ); } /// The ID of the surface that this message applies to. final String surfaceId; - /// The list of components to add or update. - final List components; - - /// Converts this object to a JSON representation. - JsonMap toJson() { - return { - surfaceIdKey: surfaceId, - 'components': components.map((c) => c.toJson()).toList(), - }; - } + /// The ID of the catalog to use for rendering this surface. + final String catalogId; + + /// The theme parameters for this surface. + final JsonMap? theme; + + /// If true, the client sends the full data model in A2A metadata. + final bool sendDataModel; + + /// Converts this message to a JSON map. + Map toJson() => { + 'version': 'v0.9', + surfaceIdKey: surfaceId, + 'catalogId': catalogId, + if (theme != null) 'theme': theme, + 'sendDataModel': sendDataModel, + }; } -/// An A2UI message that updates the data model. -final class DataModelUpdate extends A2uiMessage { - /// Creates a [DataModelUpdate] message. - const DataModelUpdate({ - required this.surfaceId, - this.path, - required this.contents, - }); +/// An A2UI message that updates a surface with new components. +final class UpdateComponents extends A2uiMessage { + /// Creates a [UpdateComponents] message. + const UpdateComponents({required this.surfaceId, required this.components}); - /// Creates a [DataModelUpdate] message from a JSON map. - factory DataModelUpdate.fromJson(JsonMap json) { - return DataModelUpdate( + /// Creates a [UpdateComponents] message from a JSON map. + factory UpdateComponents.fromJson(JsonMap json) { + return UpdateComponents( surfaceId: json[surfaceIdKey] as String, - path: json['path'] as String?, - contents: json['contents'] as Object, + components: (json['components'] as List) + .map((e) => Component.fromJson(e as JsonMap)) + .toList(), ); } /// The ID of the surface that this message applies to. final String surfaceId; - /// The path in the data model to update. - final String? path; + /// The list of components to add or update. + final List components; - /// The new contents to write to the data model. - final Object contents; + /// Converts this message to a JSON map. + Map toJson() => { + 'version': 'v0.9', + surfaceIdKey: surfaceId, + 'components': components.map((c) => c.toJson()).toList(), + }; } -/// An A2UI message that signals the client to begin rendering. -final class BeginRendering extends A2uiMessage { - /// Creates a [BeginRendering] message. - const BeginRendering({ +/// An A2UI message that updates the data model. +final class UpdateDataModel extends A2uiMessage { + /// Creates a [UpdateDataModel] message. + const UpdateDataModel({ required this.surfaceId, - required this.root, - this.styles, - this.catalogId, + this.path = DataPath.root, + this.value, }); - /// Creates a [BeginRendering] message from a JSON map. - factory BeginRendering.fromJson(JsonMap json) { - return BeginRendering( + /// Creates a [UpdateDataModel] message from a JSON map. + factory UpdateDataModel.fromJson(JsonMap json) { + return UpdateDataModel( surfaceId: json[surfaceIdKey] as String, - root: json['root'] as String, - styles: json['styles'] as JsonMap?, - catalogId: json['catalogId'] as String?, + path: DataPath(json['path'] as String? ?? '/'), + value: json['value'], ); } /// The ID of the surface that this message applies to. final String surfaceId; - /// The ID of the root component. - final String root; - - /// The styles to apply to the UI. - final JsonMap? styles; - - /// The ID of the catalog to use for rendering this surface. - final String? catalogId; + /// The path in the data model to update. Defaults to root '/'. + final DataPath path; + + /// The new value to write to the data model. + /// + /// If null (and the key is present in the JSON), it implies deletion of the + /// key at the path. + final Object? value; + + /// Converts this message to a JSON map. + Map toJson() => { + 'version': 'v0.9', + surfaceIdKey: surfaceId, + 'path': path.toString(), + if (value != null) 'value': value, + }; } /// An A2UI message that deletes a surface. -final class SurfaceDeletion extends A2uiMessage { - /// Creates a [SurfaceDeletion] message. - const SurfaceDeletion({required this.surfaceId}); +final class DeleteSurface extends A2uiMessage { + /// Creates a [DeleteSurface] message. + const DeleteSurface({required this.surfaceId}); - /// Creates a [SurfaceDeletion] message from a JSON map. - factory SurfaceDeletion.fromJson(JsonMap json) { - return SurfaceDeletion(surfaceId: json[surfaceIdKey] as String); + /// Creates a [DeleteSurface] message from a JSON map. + factory DeleteSurface.fromJson(JsonMap json) { + return DeleteSurface(surfaceId: json[surfaceIdKey] as String); } /// The ID of the surface that this message applies to. final String surfaceId; + + /// Converts this message to a JSON map. + Map toJson() => {'version': 'v0.9', surfaceIdKey: surfaceId}; } diff --git a/packages/genui/lib/src/model/a2ui_schemas.dart b/packages/genui/lib/src/model/a2ui_schemas.dart index 1c0f02c7b..fc8583602 100644 --- a/packages/genui/lib/src/model/a2ui_schemas.dart +++ b/packages/genui/lib/src/model/a2ui_schemas.dart @@ -4,248 +4,488 @@ import 'package:json_schema_builder/json_schema_builder.dart'; +import '../primitives/simple_items.dart'; import 'catalog.dart'; -import 'tools.dart'; /// Provides a set of pre-defined, reusable schema objects for common /// A2UI patterns, simplifying the creation of CatalogItem definitions. abstract final class A2uiSchemas { - /// Schema for a value that can be either a literal string or a - /// data-bound path to a string in the DataModel. If both path and - /// literal are provided, the value at the path will be initialized - /// with the literal. - /// - /// If `enumValues` are provided, the string value (either literal or at the - /// path) must be one of the values in the enum. - static Schema stringReference({ - String? description, - List? enumValues, - }) => S.object( - description: description, - properties: { - 'path': S.string( - description: 'A relative or absolute path in the data model.', - enumValues: enumValues, + /// Defines the usage of the function registry. + static Schema clientFunctions() { + return S.list( + title: 'A2UI Client Functions', + description: 'A list of functions available for use in the client.', + items: S.combined( + oneOf: [ + _requiredFunction(), + _regexFunction(), + _lengthFunction(), + _numericFunction(), + _emailFunction(), + _formatStringFunction(), + _formatNumberFunction(), + _formatCurrencyFunction(), + _formatDateFunction(), + _andFunction(), + _orFunction(), + _notFunction(), + ], ), - 'literalString': S.string(enumValues: enumValues), - }, - ); + ); + } - /// Schema for a value that can be either a literal number or a - /// data-bound path to a number in the DataModel. If both path and - /// literal are provided, the value at the path will be initialized - /// with the literal. - static Schema numberReference({String? description}) => S.object( - description: description, - properties: { - 'path': S.string( - description: 'A relative or absolute path in the data model.', - ), - 'literalNumber': S.number(), - }, - ); + static Schema _functionDefinition({ + required String name, + required String description, + required String returnType, + required Schema args, + }) { + return S.object( + description: description, + properties: { + 'call': S.string(constValue: name), + 'args': args, + 'returnType': S.string(constValue: returnType), + }, + required: ['call', 'args'], + ); + } - /// Schema for a value that can be either a literal boolean or a - /// data-bound path to a boolean in the DataModel. If both path and - /// literal are provided, the value at the path will be initialized - /// with the literal. - static Schema booleanReference({String? description}) => S.object( - description: description, - properties: { - 'path': S.string( - description: 'A relative or absolute path in the data model.', + static Schema _requiredFunction() { + return _functionDefinition( + name: 'required', + description: 'Checks that the value is not null, undefined, or empty.', + returnType: 'boolean', + args: S.object( + properties: {'value': S.any(description: 'The value to check.')}, + required: ['value'], ), - 'literalBoolean': S.boolean(), - }, - ); - - /// Schema for a property that holds a reference to a single child - /// component by its ID. - static Schema componentReference({String? description}) => - S.string(description: description); + ); + } - /// Schema for a property that holds a list of child components, - /// either as an explicit list of IDs or a data-bound template. - static Schema componentArrayReference({String? description}) => S.object( - description: description, - properties: { - 'explicitList': S.list(items: componentReference()), - 'template': S.object( - properties: {'componentId': S.string(), 'dataBinding': S.string()}, - required: ['componentId', 'dataBinding'], + static Schema _regexFunction() { + return _functionDefinition( + name: 'regex', + description: 'Checks that the value matches a regular expression string.', + returnType: 'boolean', + args: S.object( + properties: { + 'value': S.any(), // DynamicString + 'pattern': S.string( + description: 'The regex pattern to match against.', + ), + }, + required: ['value', 'pattern'], ), - }, - ); + ); + } - /// Schema for a user-initiated action, including the action name - /// and a context map of key-value pairs. - static Schema action({String? description}) => S.object( - description: description, - properties: { - 'name': S.string(), - 'context': S.list( - description: - 'A list of name-value pairs to be sent with the action to include ' - 'data associated with the action, e.g. values that are submitted.', - items: S.object( - properties: { - 'key': S.string(), - 'value': S.object( - properties: { - 'path': S.string( - description: - 'A path in the data model which should be bound to an ' - 'input element, e.g. a string reference for a text ' - 'field, or number reference for a slider.', - ), - 'literalString': S.string( - description: 'A literal string relevant to the action', - ), - 'literalNumber': S.number( - description: 'A literal number relevant to the action', - ), - 'literalBoolean': S.boolean( - description: 'A literal boolean relevant to the action', - ), - }, - ), - }, - required: ['key', 'value'], - ), + static Schema _lengthFunction() { + return _functionDefinition( + name: 'length', + description: 'Checks string length constraints.', + returnType: 'boolean', + args: S.object( + properties: { + 'value': S.any(), // DynamicString + 'min': S.integer( + minimum: 0, + description: 'The minimum allowed length.', + ), + 'max': S.integer( + minimum: 0, + description: 'The maximum allowed length.', + ), + }, + required: ['value'], ), - }, - required: ['name'], - ); + ); + } - /// Schema for a value that can be either a literal array of strings or a - /// data-bound path to an array of strings in the DataModel. If both path and - /// literalArray are provided, the value at the path will be - /// initialized with the literalArray. - static Schema stringArrayReference({String? description}) => S.object( - description: description, - properties: { - 'path': S.string( - description: 'A relative or absolute path in the data model.', + static Schema _numericFunction() { + return _functionDefinition( + name: 'numeric', + description: 'Checks numeric range constraints.', + returnType: 'boolean', + args: S.object( + properties: { + 'value': S.any(), // DynamicNumber + 'min': S.number(description: 'The minimum allowed value.'), + 'max': S.number(description: 'The maximum allowed value.'), + }, + required: ['value'], ), - 'literalArray': S.list(items: S.string()), - }, - ); + ); + } - /// Schema for a beginRendering message, which provides the root widget ID for - /// the given surface so that the surface can be rendered. - static Schema beginRenderingSchema() => S.object( - properties: { - surfaceIdKey: S.string( - description: 'The surface ID of the surface to render.', - ), - 'root': S.string( - description: - 'The root widget ID for the surface. ' - 'All components must be descendents of this root in order to be ' - 'displayed.', + static Schema _emailFunction() { + return _functionDefinition( + name: 'email', + description: 'Checks that the value is a valid email address.', + returnType: 'boolean', + args: S.object( + properties: { + 'value': S.any(), // DynamicString + }, + required: ['value'], ), - 'catalogId': S.string( - description: - 'The identifier of the component catalog to use for this surface.', + ); + } + + static Schema _formatStringFunction() { + return _functionDefinition( + name: 'formatString', + description: + '''Performs string interpolation of data model values and other functions.''', + returnType: 'string', + args: S.object( + properties: {'value': S.any(description: 'The string to format.')}, + required: ['value'], + additionalProperties: true, // Allow other interpolation args ), - 'styles': S.object( + ); + } + + static Schema _formatNumberFunction() { + return _functionDefinition( + name: 'formatNumber', + description: + 'Formats a number with the specified grouping and decimal precision.', + returnType: 'string', + args: S.object( properties: { - 'font': S.string(description: 'The base font for this surface'), - 'primaryColor': S.string( - description: 'The seed color for the theme of this surface.', + 'value': S.number(description: 'The number to format.'), + 'decimalPlaces': S.integer( + description: 'Optional. The number of decimal places to show.', + ), + 'useGrouping': S.boolean( + description: + '''Optional. If true, uses locale-specific grouping separators.''', ), }, + required: ['value'], ), - }, - required: [surfaceIdKey, 'root'], - ); + ); + } - /// Schema for a beginRendering message, which provides the root widget ID for - /// the given surface so that the surface can be rendered. - static Schema beginRenderingSchemaNoCatalogId() => S.object( - properties: { - surfaceIdKey: S.string( - description: 'The surface ID of the surface to render.', - ), - 'root': S.string( - description: - 'The root widget ID for the surface. ' - 'All components must be descendents of this root in order to be ' - 'displayed.', + static Schema _formatCurrencyFunction() { + return _functionDefinition( + name: 'formatCurrency', + description: 'Formats a number as a currency string.', + returnType: 'string', + args: S.object( + properties: { + 'value': S.number(description: 'The monetary amount.'), + 'currencyCode': S.string( + description: "The ISO 4217 currency code (e.g., 'USD', 'EUR').", + ), + }, + required: ['value', 'currencyCode'], ), - 'styles': S.object( + ); + } + + static Schema _formatDateFunction() { + return _functionDefinition( + name: 'formatDate', + description: 'Formats a timestamp into a string using a pattern.', + returnType: 'string', + args: S.object( properties: { - 'font': S.string(description: 'The base font for this surface'), - 'primaryColor': S.string( - description: 'The seed color for the theme of this surface.', + 'value': S.any(description: 'The date to format.'), + 'pattern': S.string( + description: 'The format pattern (e.g. "MM/dd/yyyy").', ), }, + required: ['value', 'pattern'], ), - }, - required: [surfaceIdKey, 'root'], - ); + ); + } - /// Schema for a `deleteSurface` message which will delete the given surface. - static Schema surfaceDeletionSchema() => S.object( - properties: {surfaceIdKey: S.string()}, - required: [surfaceIdKey], - ); + static Schema _andFunction() { + return _functionDefinition( + name: 'and', + description: 'Performs logical AND on a list of values.', + returnType: 'boolean', + args: S.object( + properties: {'values': S.list(items: S.any(), minItems: 2)}, + required: ['values'], + ), + ); + } + + static Schema _orFunction() { + return _functionDefinition( + name: 'or', + description: 'Performs logical OR on a list of values.', + returnType: 'boolean', + args: S.object( + properties: {'values': S.list(items: S.any(), minItems: 2)}, + required: ['values'], + ), + ); + } + + static Schema _notFunction() { + return _functionDefinition( + name: 'not', + description: 'Performs logical NOT on a value.', + returnType: 'boolean', + args: S.object(properties: {'value': S.any()}, required: ['value']), + ); + } - /// Schema for a `dataModelUpdate` message which will update the given path in - /// the data model. If the path is omitted, the entire data model is replaced. - static Schema dataModelUpdateSchema() => S.object( + /// Schema for a function call. + static Schema functionCall() => S.object( properties: { - surfaceIdKey: S.string(), - 'path': S.string(), - 'contents': S.any( - description: 'The new contents to write to the data model.', + 'call': S.string(description: 'The name of the function to call.'), + 'args': S.object( + description: 'Arguments to pass to the function.', + additionalProperties: true, ), }, - required: [surfaceIdKey, 'contents'], + required: ['call'], ); - /// Schema for a `surfaceUpdate` message which defines the components to be - /// rendered on a surface. - static Schema surfaceUpdateSchema(Catalog catalog) => S.object( - properties: { - surfaceIdKey: S.string( - description: - 'The unique identifier for the UI surface to create or ' - 'update. If you are adding a new surface this *must* be a ' - 'new, unique identified that has never been used for any ' - 'existing surfaces shown.', - ), - 'components': S.list( - description: 'A list of component definitions.', - minItems: 1, - items: S.object( + /// Schema for a validation check, including logic and an error message. + static Schema validationCheck({String? description}) { + return S.object( + description: description, + properties: { + 'message': S.string(description: 'Error message if validation fails.'), + 'condition': S.any( description: - 'Represents a *single* component in a UI widget tree. ' - 'This component could be one of many supported types.', + 'DynamicBoolean condition (FunctionCall, DataBinding, or ' + 'literal).', + ), + }, + required: ['message', 'condition'], + ); + } + + /// Schema for a value that can be either a literal string or a + /// data-bound path to a string in the DataModel. + static Schema stringReference({ + String? description, + List? enumValues, + }) { + final literal = S.string( + description: 'A literal string value.', + enumValues: enumValues, + ); + final Schema binding = dataBindingSchema( + description: 'A path to a string.', + ); + final Schema function = functionCall(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } + + /// Schema for a value that can be either a literal number or a + /// data-bound path to a number in the DataModel. + static Schema numberReference({String? description}) { + final literal = S.number(description: 'A literal number value.'); + final Schema binding = dataBindingSchema( + description: 'A path to a number.', + ); + final Schema function = functionCall(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } + + /// Schema for a value that can be either a literal boolean or a + /// data-bound path to a boolean in the DataModel. + static Schema booleanReference({String? description}) { + final literal = S.boolean(description: 'A literal boolean value.'); + final Schema binding = dataBindingSchema( + description: 'A path to a boolean.', + ); + final Schema function = functionCall(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } + + /// Helper to create a DataBinding schema. + static Schema dataBindingSchema({String? description}) { + return S.object( + description: description, + properties: { + 'path': S.string( + description: 'A relative or absolute path in the data model.', + ), + }, + required: ['path'], + ); + } + + /// Schema for a property that holds a list of child components, + /// either as an explicit list of IDs or a data-bound template. + static Schema componentArrayReference({String? description}) { + final idList = S.list(items: S.string(description: 'Component ID')); + final template = S.object( + properties: { + 'componentId': componentReference(), + 'path': S.string( + description: 'A relative or absolute path in the data model.', + ), + }, + required: ['componentId', 'path'], + ); + return S.combined(oneOf: [idList, template], description: description); + } + + /// Schema for a list of validation checks. + static Schema checkable({String? description}) { + return S.list( + description: description ?? 'Validation rules for this component.', + items: validationCheck(), + ); + } + + /// Schema for a user-initiated action. + /// + /// Can be either a server-side event or a client-side function call. + static Schema action({String? description}) { + final eventSchema = S.object( + properties: { + 'event': S.object( properties: { - 'id': S.string(), - 'weight': S.integer( + 'name': S.string( description: - 'Optional layout weight for use in Row/Column children.', + 'The name of the action to be dispatched to the server.', ), - 'component': S.object( - description: - '''A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Text'). The value is an object containing the properties for that specific component.''', - properties: { - for (var entry - in ((catalog.definition as ObjectSchema) - .properties!['components']! - as ObjectSchema) - .properties! - .entries) - entry.key: entry.value, - }, + 'context': S.object( + description: 'Arbitrary context data to send with the action.', + additionalProperties: true, ), }, - required: ['id', 'component'], + required: ['name'], ), + }, + required: ['event'], + ); + + final functionCallSchema = S.object( + properties: {'functionCall': functionCall()}, + required: ['functionCall'], + ); + + return S.combined( + description: description, + oneOf: [eventSchema, functionCallSchema], + ); + } + + /// Schema for a value that can be either a literal array of strings or a + /// data-bound path to an array of strings. + static Schema stringArrayReference({String? description}) { + final literal = S.list(items: S.string()); + final Schema binding = dataBindingSchema( + description: 'A path to a string list.', + ); + final Schema function = functionCall(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } + + /// Schema for a createSurface message. + static Schema createSurfaceSchema() => S.object( + properties: { + surfaceIdKey: S.string(description: 'The unique ID for the surface.'), + 'catalogId': S.string(description: 'The URI of the component catalog.'), + 'theme': S.object( + description: 'Theme parameters for the surface.', + additionalProperties: true, + ), + 'sendDataModel': S.boolean( + description: 'Whether to send the data model to every client request.', ), }, - required: [surfaceIdKey, 'components'], + required: [surfaceIdKey, 'catalogId'], + ); + + /// Schema for a deleteSurface message. + static Schema deleteSurfaceSchema() => S.object( + properties: {surfaceIdKey: S.string()}, + required: [surfaceIdKey], ); + + /// Schema for a updateDataModel message. + static Schema updateDataModelSchema() => S.object( + properties: { + surfaceIdKey: S.string(), + 'path': S.combined(type: JsonType.string, defaultValue: '/'), + 'value': S.any( + description: + 'The new value to write to the data model. If null/omitted, the key is removed.', + ), + }, + required: [surfaceIdKey], + ); + + /// Schema for a component reference (ID). + static Schema componentReference({String? description}) { + return S.string(description: description ?? 'The ID of a component.'); + } + + /// Schema for a updateComponents message. + static Schema updateComponentsSchema(Catalog catalog) { + // Collect specific component schemas from the catalog. + // We assume catalog items have updated schemas (flattened). + final List componentSchemas = catalog.items + .map((item) => item.dataSchema) + .toList(); + + return S.object( + properties: { + surfaceIdKey: S.string( + description: 'The unique identifier for the UI surface.', + ), + 'components': S.list( + description: 'A flat list of component definitions.', + minItems: 1, + items: componentSchemas.isEmpty + ? S.object(description: 'No components in catalog.') + : S.combined( + oneOf: componentSchemas, + description: + 'Must match one of the component definitions in the ' + 'catalog.', + ), + ), + }, + required: [surfaceIdKey, 'components'], + ); + } + + /// Schema for a value that can be either a literal list or a reference. + static Schema listOrReference({required Schema items, String? description}) { + final literal = S.list(items: items); + final Schema binding = dataBindingSchema(description: 'A path to a list.'); + final Schema function = functionCall(); + return S.combined( + oneOf: [literal, binding, function], + description: description, + ); + } + + /// Schema for a generic property value (literal, binding, or function). + static Schema propertyReference({String? description}) { + final Schema binding = dataBindingSchema(description: 'A path to a value.'); + final Schema function = functionCall(); + // We allow any type for the literal value since we don't know it here. + // Ideally usage would be more specific if possible. + return S.combined( + oneOf: [S.any(), binding, function], + description: description, + ); + } } diff --git a/packages/genui/lib/src/model/basic_catalog_embed.dart b/packages/genui/lib/src/model/basic_catalog_embed.dart new file mode 100644 index 000000000..9120c957a --- /dev/null +++ b/packages/genui/lib/src/model/basic_catalog_embed.dart @@ -0,0 +1,63 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A static container for the basic catalog and rules, embedded to avoid +/// file I/O and duplication across providers. +class BasicCatalogEmbed { + /// The text content of basic_catalog_rules.txt. + static const String basicCatalogRules = r''' +**REQUIRED PROPERTIES:** You MUST include ALL required properties for every component, even if they are inside a template or will be bound to data. +- For 'Text', you MUST provide 'text'. If dynamic, use { "path": "..." }. +- For 'Image', you MUST provide 'url'. If dynamic, use { "path": "..." }. +- For 'Button', you MUST provide 'action'. +- For 'TextField', 'CheckBox', etc., you MUST provide 'label'. + +**OUTPUT FORMAT:** +You must output a VALID JSON object representing one of the A2UI message types (`createSurface`, `updateComponents`, `updateDataModel`, `deleteSurface`). +- Do NOT use function blocks or tool calls for these messages. +- You can treat the A2UI schema as a specification for the JSON you typically output. +- You may include a brief conversational explanation before or after the JSON block if it helps the user, but the JSON block must be valid and complete. +- Ensure your JSON is fenced with ```json and ```. + +**EXAMPLES:** + +1. Create a surface: +```json +{ + "createSurface": { + "surfaceId": "main", + "catalogId": "https://a2ui.org/specification/v0_9/standard_catalog.json", + "sendDataModel": true + } +} +``` + +2. Update components: +```json +{ + "updateComponents": { + "surfaceId": "main", + "components": [ + { + // The root component MUST have id "root" + "id": "root", + "component": "Column", + "justify": "start", + "children": [ + "headerText", + "content" + ] + } + ] + } +} +``` + +**IMPORTANT:** +- One of the components sent in one of the `updateComponents` MUST have id "root", or nothing will be displayed. +- Do NOT nest `components` inside `createSurface`. Use `updateComponents` to add components to a surface. +- `createSurface` ONLY sets up the surface (ID and catalog). It does NOT take content. +- To show a UI, you typically send a `createSurface` message (if the surface doesn't exist), followed by an `updateComponents` message. +'''; +} diff --git a/packages/genui/lib/src/model/catalog.dart b/packages/genui/lib/src/model/catalog.dart index d16c88de2..c6f661841 100644 --- a/packages/genui/lib/src/model/catalog.dart +++ b/packages/genui/lib/src/model/catalog.dart @@ -8,6 +8,7 @@ import 'package:json_schema_builder/json_schema_builder.dart'; import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; +import 'a2ui_schemas.dart'; import 'catalog_item.dart'; import 'data_model.dart'; @@ -15,21 +16,24 @@ import 'data_model.dart'; /// to construct a user interface. /// /// A [Catalog] serves three primary purposes: -/// 1. It holds a list of [CatalogItem]s, which define the available widgets. -/// 2. It provides a mechanism to build a Flutter widget from a JSON-like data +/// +/// 1. Holds a list of [CatalogItem]s, which define the available widgets. +/// 2. Provides a mechanism to build a Flutter widget from a JSON-like data /// structure ([JsonMap]). -/// 3. It dynamically generates a [Schema] that describes the structure of all +/// 3. Dynamically generates a [Schema] that describes the structure of all /// supported widgets, which can be provided to the AI model. @immutable -class Catalog { +interface class Catalog { /// Creates a new catalog with the given list of items. const Catalog(this.items, {this.catalogId}); /// The list of [CatalogItem]s available in this catalog. final Iterable items; - /// A string that uniquely identifies this catalog. It is recommended to use - /// a reverse-domain name notation, e.g. 'com.example.my_catalog'. + /// A string that uniquely identifies this catalog. + /// + /// The recommended format for this string is reverse-domain name notation, + /// e.g. 'com.example.my_catalog'. final String? catalogId; /// Returns a new [Catalog] containing the items from both this catalog and @@ -59,31 +63,35 @@ class Catalog { /// Builds a Flutter widget from a JSON-like data structure. Widget buildWidget(CatalogItemContext itemContext) { - final widgetData = itemContext.data as JsonMap; - final String? widgetType = widgetData.keys.firstOrNull; + final String widgetType = itemContext.type; final CatalogItem? item = items.firstWhereOrNull( (item) => item.name == widgetType, ); if (item == null) { - genUiLogger.severe('Item $widgetType was not found in catalog'); - return Container(); + throw CatalogItemNotFoundException(widgetType, catalogId: catalogId); } genUiLogger.info('Building widget ${item.name} with id ${itemContext.id}'); - return item.widgetBuilder( - CatalogItemContext( - data: JsonMap.from(widgetData[widgetType]! as Map), - id: itemContext.id, - buildChild: (String childId, [DataContext? childDataContext]) => - itemContext.buildChild( - childId, - childDataContext ?? itemContext.dataContext, - ), - dispatchEvent: itemContext.dispatchEvent, - buildContext: itemContext.buildContext, - dataContext: itemContext.dataContext, - getComponent: itemContext.getComponent, - surfaceId: itemContext.surfaceId, + return KeyedSubtree( + key: ValueKey(itemContext.id), + child: item.widgetBuilder( + CatalogItemContext( + data: itemContext.data, + id: itemContext.id, + type: widgetType, + buildChild: (String childId, [DataContext? childDataContext]) => + itemContext.buildChild( + childId, + childDataContext ?? itemContext.dataContext, + ), + dispatchEvent: itemContext.dispatchEvent, + buildContext: itemContext.buildContext, + dataContext: itemContext.dataContext, + getComponent: itemContext.getComponent, + getCatalogItem: (String type) => + items.firstWhereOrNull((item) => item.name == type), + surfaceId: itemContext.surfaceId, + ), ), ); } @@ -122,8 +130,34 @@ class Catalog { 'properties.', properties: {}, ), + 'functions': A2uiSchemas.clientFunctions(), }, - required: ['components', 'styles'], + required: ['components', 'styles', 'functions'], + ); + } +} + +/// An exception thrown when a requested item is not found in the [Catalog]. +class CatalogItemNotFoundException implements Exception { + /// Creates a new [CatalogItemNotFoundException]. + const CatalogItemNotFoundException(this.widgetType, {this.catalogId}); + + /// The type of the widget that was not found. + final String widgetType; + + /// The ID of the catalog that was searched. + final String? catalogId; + + @override + String toString() { + final buffer = StringBuffer(); + buffer.write( + 'CatalogItemNotFoundException: Item "$widgetType" ' + 'was not found in catalog', ); + if (catalogId != null) { + buffer.write(' "$catalogId"'); + } + return buffer.toString(); } } diff --git a/packages/genui/lib/src/model/catalog_item.dart b/packages/genui/lib/src/model/catalog_item.dart index b1a18a9dc..00caaf412 100644 --- a/packages/genui/lib/src/model/catalog_item.dart +++ b/packages/genui/lib/src/model/catalog_item.dart @@ -31,7 +31,7 @@ typedef CatalogWidgetBuilder = Widget Function(CatalogItemContext itemContext); /// a catalog widget, including access to the widget's data, its position in /// the component tree, and mechanisms for building children and dispatching /// events. -class CatalogItemContext { +final class CatalogItemContext { /// Creates a [CatalogItemContext] with the required parameters. /// /// All parameters are required to ensure the widget builder has complete @@ -39,11 +39,13 @@ class CatalogItemContext { CatalogItemContext({ required this.data, required this.id, + required this.type, required this.buildChild, required this.dispatchEvent, required this.buildContext, required this.dataContext, required this.getComponent, + required this.getCatalogItem, required this.surfaceId, }); @@ -53,6 +55,9 @@ class CatalogItemContext { /// The unique identifier for this component instance. final String id; + /// The type of this component. + final String type; + /// Callback to build a child widget by its component ID. final ChildBuilderCallback buildChild; @@ -68,19 +73,23 @@ class CatalogItemContext { /// Callback to retrieve a component definition by its ID. final GetComponentCallback getComponent; + /// Callback to retrieve a catalog item definition by its type name. + final CatalogItem? Function(String type) getCatalogItem; + /// The ID of the surface this component belongs to. final String surfaceId; } /// Defines a UI layout type, its schema, and how to build its widget. @immutable -class CatalogItem { +final class CatalogItem { /// Creates a new [CatalogItem]. const CatalogItem({ required this.name, required this.dataSchema, required this.widgetBuilder, this.exampleData = const [], + this.isImplicitlyFlexible = false, }); /// The widget type name used in JSON, e.g., 'TextChatMessage'. @@ -92,6 +101,16 @@ class CatalogItem { /// The builder for this widget. final CatalogWidgetBuilder widgetBuilder; + /// Whether this component should be implicitly flexible when placed in a flex + /// container (like Row/Column). + /// + /// If true, a [Row] or [Column] will automatically assign a flex weight to + /// this component if one is not explicitly provided, wrapping it in a + /// [Flexible] widget. + /// This is useful for components that require bounded constraints, like + /// [TextField] or [ListView]. + final bool isImplicitlyFlexible; + /// A list of builder functions that each return a JSON string representing an /// example usage of this widget. /// diff --git a/packages/genui/lib/src/model/chat_message.dart b/packages/genui/lib/src/model/chat_message.dart index 5038bba58..59c7f9374 100644 --- a/packages/genui/lib/src/model/chat_message.dart +++ b/packages/genui/lib/src/model/chat_message.dart @@ -2,249 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:typed_data'; +import 'package:genai_primitives/genai_primitives.dart'; -import 'package:flutter/material.dart'; +export 'package:genai_primitives/genai_primitives.dart'; -import '../primitives/simple_items.dart'; -import 'ui_models.dart'; +// Re-export UI helpers +export 'parts/ui.dart'; -/// A sealed class representing a part of a message. -/// -/// This allows for multi-modal content in a single message. -sealed class MessagePart {} +/// Extension to help with `UserMessage` usages if needed, +/// or just helper factories. +extension ChatMessageFactories on ChatMessage { + /// Creates a text message from a user. + static ChatMessage userText(String text) => ChatMessage.user(text); -/// A text part of a message. -final class TextPart implements MessagePart { - /// The text content. - final String text; - - /// Creates a [TextPart] with the given [text]. - const TextPart(this.text); -} - -/// A data part that can send structured data to the model. -final class DataPart implements MessagePart { - /// The data content. - final Map? data; - - /// Creates a [DataPart] with the given [data]. - const DataPart(this.data); -} - -/// An image part of a message. -/// -/// Use the factory constructors to create an instance from different sources. -final class ImagePart implements MessagePart { - /// The raw image bytes. May be null if created from a URL or Base64. - final Uint8List? bytes; - - /// The Base64 encoded image string. May be null if created from bytes or URL. - final String? base64; - - /// The URL of the image. May be null if created from bytes or Base64. - final Uri? url; - - /// The MIME type of the image (e.g., 'image/jpeg', 'image/png'). - /// Required when providing image data directly. - final String mimeType; - - // Private constructor to enforce creation via factories. - const ImagePart._({ - this.bytes, - this.base64, - this.url, - required this.mimeType, - }); - - /// Creates an [ImagePart] from raw image bytes. - const factory ImagePart.fromBytes( - Uint8List bytes, { - required String mimeType, - }) = _ImagePartFromBytes; - - /// Creates an [ImagePart] from a Base64 encoded string. - const factory ImagePart.fromBase64( - String base64, { - required String mimeType, - }) = _ImagePartFromBase64; - - /// Creates an [ImagePart] from a URL. - const factory ImagePart.fromUrl(Uri url, {required String mimeType}) = - _ImagePartFromUrl; -} - -// Private implementation classes for ImagePart factories -final class _ImagePartFromBytes extends ImagePart { - const _ImagePartFromBytes(Uint8List bytes, {required super.mimeType}) - : super._(bytes: bytes); -} - -final class _ImagePartFromBase64 extends ImagePart { - const _ImagePartFromBase64(String base64, {required super.mimeType}) - : super._(base64: base64); -} - -final class _ImagePartFromUrl extends ImagePart { - const _ImagePartFromUrl(Uri url, {required super.mimeType}) - : super._(url: url); -} - -/// A part representing a request from the model to call a tool. -final class ToolCallPart implements MessagePart { - /// The name of the tool to call. - final String toolName; - - /// The arguments for the tool, as a JSON-like map. - final Map arguments; - - /// A unique identifier for this specific tool call. - final String id; - - /// Creates a [ToolCallPart] with the given [id], [toolName], and - /// [arguments]. - const ToolCallPart({ - required this.id, - required this.toolName, - required this.arguments, - }); -} - -/// A part representing the result of a tool call, to be sent back to the model. -final class ToolResultPart implements MessagePart { - /// The ID of the this result corresponds to. - final String callId; - - /// The result of the tool execution, often a JSON string. - final String result; - - /// Creates a [ToolResultPart] with the given [callId] and [result]. - const ToolResultPart({required this.callId, required this.result}); -} - -/// A provider-specific part for "thinking" blocks. -final class ThinkingPart implements MessagePart { - /// The reasoning content from the model. - final String text; - - /// Creates a [ThinkingPart] with the given [text]. - const ThinkingPart(this.text); -} - -/// A sealed class representing a message in the chat history. -sealed class ChatMessage { - /// Creates a [ChatMessage]. - const ChatMessage(); -} - -/// A message representing an internal message -final class InternalMessage extends ChatMessage { - /// Creates a [InternalMessage] with the given [text]. - const InternalMessage(this.text); - - /// The text of the system message. - final String text; -} - -/// A message representing a user's message. -/// -/// It can be a text message, or selections in UI. -final class UserMessage extends ChatMessage { - /// Creates a [UserMessage] with the given [parts]. - UserMessage(this.parts); - - /// Creates a [UserMessage] with the given [text]. - factory UserMessage.text(String text) => UserMessage([TextPart(text)]); - - /// The parts of the user's message. - final List parts; - - /// The text content of the user's message. - late final String text = parts - .whereType() - .map((p) => p.text) - .join('\n'); -} - -/// A message representing a user's interaction with the UI. -/// -/// This is intended for internal use and is not typically displayed to the -/// user. -final class UserUiInteractionMessage extends ChatMessage { - /// Creates a [UserUiInteractionMessage] with the given [parts]. - UserUiInteractionMessage(this.parts); - - /// Creates a [UserUiInteractionMessage] with the given [text]. - factory UserUiInteractionMessage.text(String text) => - UserUiInteractionMessage([TextPart(text)]); - - /// The parts of the user's message. - final List parts; - - /// The text content of the UI interaction. - late final String text = parts - .whereType() - .map((p) => p.text) - .join('\n'); -} - -/// A message representing a text response from the AI. -final class AiTextMessage extends ChatMessage { - /// Creates a [AiTextMessage] with the given [parts]. - AiTextMessage(this.parts); - - /// Creates a [AiTextMessage] with the given [text]. - factory AiTextMessage.text(String text) => AiTextMessage([TextPart(text)]); - - /// The parts of the AI's message. - final List parts; - - /// The text content of the AI's message. - late final String text = parts - .whereType() - .map((p) => p.text) - .join('\n'); -} - -/// A message representing a response from a tool. - -final class ToolResponseMessage extends ChatMessage { - /// Creates a [ToolResponseMessage] with the given [results]. - - const ToolResponseMessage(this.results); - - /// The results of the tool calls. - - final List results; -} - -/// A message representing a UI response from the AI. - -final class AiUiMessage extends ChatMessage { - /// Creates a [AiUiMessage] with the given UI [definition]. - - AiUiMessage({required this.definition, String? surfaceId}) - : uiKey = UniqueKey(), - - parts = [TextPart(definition.asContextDescriptionText())], - - surfaceId = surfaceId ?? generateId(); - - /// The JSON definition of the UI. - - final UiDefinition definition; - - /// A unique key for the UI widget. - - final Key uiKey; - - /// The unique ID for this UI surface. - - final String surfaceId; - - /// The parts of this message, containing a text description of the UI. - /// - /// This is automatically generated from the [definition] and provides - /// context for the AI about the current UI state. - final List parts; + /// Creates a text message from the model. + static ChatMessage modelText(String text) => ChatMessage.model(text); } diff --git a/packages/genui/lib/src/model/data_model.dart b/packages/genui/lib/src/model/data_model.dart index e00fc7298..a24232493 100644 --- a/packages/genui/lib/src/model/data_model.dart +++ b/packages/genui/lib/src/model/data_model.dart @@ -4,14 +4,20 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import '../functions/expression_parser.dart'; + import '../primitives/logging.dart'; import '../primitives/simple_items.dart'; +/// Represents a path in the data model, either absolute or relative. @immutable -class DataPath { +final class DataPath { + /// Creates a [DataPath] from a string representation. factory DataPath(String path) { + if (path == _separator) return root; final List segments = path .split(_separator) .where((s) => s.isNotEmpty) @@ -21,17 +27,25 @@ class DataPath { const DataPath._(this.segments, this.isAbsolute); + /// The segments of the path. final List segments; + + /// Whether the path is absolute (starts with a separator). final bool isAbsolute; static const String _separator = '/'; + + /// The root path. static const DataPath root = DataPath._([], true); + /// The last segment of the path. String get basename => segments.last; + /// The path without the last segment. DataPath get dirname => DataPath._(segments.sublist(0, segments.length - 1), isAbsolute); + /// Joins this path with another path. DataPath join(DataPath other) { if (other.isAbsolute) { return other; @@ -39,6 +53,7 @@ class DataPath { return DataPath._([...segments, ...other.segments], isAbsolute); } + /// Returns whether this path starts with the other path. bool startsWith(DataPath other) { if (other.segments.length > segments.length) { return false; @@ -66,86 +81,179 @@ class DataPath { listEquals(segments, other.segments); @override - int get hashCode => Object.hash(isAbsolute, Object.hashAll(segments)); + int get hashCode => + Object.hash(isAbsolute, const DeepCollectionEquality().hash(segments)); } /// A contextual view of the main DataModel, used by widgets to resolve /// relative and absolute paths. class DataContext { + /// Creates a [DataContext] for the given [path]. DataContext(this._dataModel, String path) : path = DataPath(path); DataContext._(this._dataModel, this.path); final DataModel _dataModel; + + /// The path associated with this context. final DataPath path; - /// Subscribes to a path, resolving it against the current context. - ValueNotifier subscribe(DataPath relativeOrAbsolutePath) { - final DataPath absolutePath = resolvePath(relativeOrAbsolutePath); - return _dataModel.subscribe(absolutePath); + /// The underlying data model for this context. + DataModel get dataModel => _dataModel; + + /// Subscribes to a path or expression, resolving it against the current + /// context. + /// + /// If [pathOrExpression] contains `${`, it is treated as an expression. + /// Otherwise, it is treated as a path. + ValueNotifier subscribe(Object? pathOrExpression) { + if (pathOrExpression is String && pathOrExpression.contains(r'${')) { + // Expressions require reactivity based on their dependencies. + // Since `ExpressionParser` doesn't currently return dependencies, we use + // a `_ComputedValueNotifier` that attempts to extract paths from the + // expression. + return createComputedNotifier(pathOrExpression); + } else if (pathOrExpression is Map) { + // Map expressions (e.g. function calls) + return createComputedNotifier(pathOrExpression); + } else if (pathOrExpression is String) { + final DataPath absolutePath = resolvePath(DataPath(pathOrExpression)); + return _dataModel.subscribe(absolutePath); + } + return ValueNotifier(pathOrExpression as T?); } - /// Gets a static value, resolving the path against the current context. - T? getValue(DataPath relativeOrAbsolutePath) { - final DataPath absolutePath = resolvePath(relativeOrAbsolutePath); - return _dataModel.getValue(absolutePath); + /// Gets a value, resolving the path/expression against the current context. + T? getValue(Object? pathOrExpression) { + if (pathOrExpression is String && pathOrExpression.contains(r'${')) { + final parser = ExpressionParser(this); + return parser.parse(pathOrExpression) as T?; + } else if (pathOrExpression is Map) { + final parser = ExpressionParser(this); + return parser.evaluate(pathOrExpression) as T?; + } else if (pathOrExpression is String) { + final DataPath absolutePath = resolvePath(DataPath(pathOrExpression)); + return _dataModel.getValue(absolutePath); + } + return pathOrExpression as T?; } /// Updates the data model, resolving the path against the current context. - void update(DataPath relativeOrAbsolutePath, Object? contents) { - final DataPath absolutePath = resolvePath(relativeOrAbsolutePath); + void update(String pathStr, Object? contents) { + final DataPath absolutePath = resolvePath(DataPath(pathStr)); _dataModel.update(absolutePath, contents); } /// Creates a new, nested DataContext for a child widget. - /// Used by list/template widgets for their children. - DataContext nested(DataPath relativePath) { - final DataPath newPath = resolvePath(relativePath); + /// + /// Used by list/template widgets to create a context for their children. + DataContext nested(String relativePath) { + final DataPath newPath = resolvePath(DataPath(relativePath)); return DataContext._(_dataModel, newPath); } + /// Resolves a path against the current context's path. DataPath resolvePath(DataPath pathToResolve) { if (pathToResolve.isAbsolute) { return pathToResolve; } return path.join(pathToResolve); } + + /// Resolves any expressions in the given value. + Object? resolve(Object? value) { + if (value is String) { + return ExpressionParser(this).parse(value); + } + if (value is Map && value.containsKey('call')) { + return ExpressionParser(this).evaluateFunctionCall(value as JsonMap); + } + return value; + } + + ValueNotifier createComputedNotifier(Object? expression) { + // Create a notifier that re-evaluates the expression when its dependencies + // change. + return _ComputedValueNotifier(this, expression); + } +} + +class _ComputedValueNotifier extends ValueNotifier { + _ComputedValueNotifier(this.context, this.expression) : super(null) { + initialEvaluation(); + } + + final DataContext context; + final Object? expression; + final List unsubscribers = []; + + void initialEvaluation() { + // Use ExpressionParser to robustly extract paths, including those in + // function calls and nested expressions. + final Set paths = ExpressionParser( + context, + ).extractDependenciesFrom(expression); + + for (final path in paths) { + final ValueNotifier notifier = context.subscribe( + path.toString(), + ); // Re-enter subscribe for raw paths + void listener() => evaluate(); + notifier.addListener(listener); + unsubscribers.add(() => notifier.removeListener(listener)); + } + evaluate(); + } + + void evaluate() { + final parser = ExpressionParser(context); + final Object? result = parser.evaluate(expression); + value = result as T?; + } + + @override + void dispose() { + for (final VoidCallback unsub in unsubscribers) { + unsub(); + } + super.dispose(); + } } -/// Manages the application's Object? data model and provides -/// a subscription-based mechanism for reactive UI updates. -class DataModel { +/// Manages the application's data model and provides a subscription-based +/// mechanism for reactive UI updates. +interface class DataModel { JsonMap _data = {}; final Map> _subscriptions = {}; - final Map> _valueSubscriptions = {}; + + final List _cleanupCallbacks = []; /// The full contents of the data model. JsonMap get data => _data; /// Updates the data model at a specific absolute path and notifies all /// relevant subscribers. + /// + /// If [absolutePath] is null or root, the entire data model is replaced + /// (if contents is a Map). void update(DataPath? absolutePath, Object? contents) { genUiLogger.info( 'DataModel.update: path=$absolutePath, contents=' '${const JsonEncoder.withIndent(' ').convert(contents)}', ); - if (absolutePath == null || absolutePath.segments.isEmpty) { - if (contents is List) { - _data = _parseDataModelContents(contents); - } else if (contents is Map) { - // Permissive: Allow a map to be sent for the root, even though the - // schema expects a list. - genUiLogger.info( - 'DataModel.update: contents for root path is a Map, not a ' - 'List: $contents', - ); + + if (absolutePath == null || + absolutePath.segments.isEmpty || + absolutePath == DataPath.root) { + if (contents is Map) { _data = Map.from(contents); } else { genUiLogger.warning( - 'DataModel.update: contents for root path is not a List or ' - 'Map: $contents', + 'DataModel.update: contents for root path is not a Map: $contents', ); - _data = {}; // Fallback + if (contents == null) { + _data = {}; + } } _notifySubscribers(DataPath.root); return; @@ -157,105 +265,96 @@ class DataModel { /// Subscribes to a specific absolute path in the data model. ValueNotifier subscribe(DataPath absolutePath) { - genUiLogger.info('DataModel.subscribe: path=$absolutePath'); + genUiLogger.finer('DataModel.subscribe: path=$absolutePath'); final T? initialValue = getValue(absolutePath); if (_subscriptions.containsKey(absolutePath)) { final notifier = _subscriptions[absolutePath]! as ValueNotifier; - notifier.value = initialValue; - return notifier; - } - final notifier = ValueNotifier(initialValue); - _subscriptions[absolutePath] = notifier; - return notifier; - } - /// Subscribes to a specific absolute path in the data model, only notifying - /// when the value at that exact path changes. - ValueNotifier subscribeToValue(DataPath absolutePath) { - genUiLogger.info('DataModel.subscribeToValue: path=$absolutePath'); - final T? initialValue = getValue(absolutePath); - if (_valueSubscriptions.containsKey(absolutePath)) { - final notifier = _valueSubscriptions[absolutePath]! as ValueNotifier; - notifier.value = initialValue; return notifier; } final notifier = ValueNotifier(initialValue); - _valueSubscriptions[absolutePath] = notifier; + _subscriptions[absolutePath] = notifier; return notifier; } - /// Retrieves a static, one-time value from the data model at the - /// specified absolute path without creating a subscription. - T? getValue(DataPath absolutePath) { - return _getValue(_data, absolutePath.segments) as T?; - } + final List _externalSubscriptions = []; - /// Parses a list of content objects into a [JsonMap]. + /// Binds an external state [source] to a [path] in the DataModel. /// - /// Each item in [contents] is expected to be a `Map` - /// with a 'key' and a single 'valueString', 'valueNumber', 'valueBoolean', - /// or 'valueMap' entry. - JsonMap _parseDataModelContents(List contents) { - final newData = {}; - for (final item in contents) { - if (item is! Map || !item.containsKey('key')) { - genUiLogger.warning('Invalid item in dataModelUpdate contents: $item'); - continue; + /// If [twoWay] is true, changes in the DataModel at [path] will also + /// update the [source] (assuming [source] is a [ValueNotifier]). + void bindExternalState({ + required DataPath path, + required ValueListenable source, + bool twoWay = false, + }) { + update(path, source.value); + + void onSourceChanged() { + final T newValue = source.value; + final T? currentValue = getValue(path); + if (currentValue != newValue) { + update(path, newValue); } + } - final key = item['key'] as String; - Object? value; - var valueCount = 0; - - const valueKeys = [ - 'valueString', - 'valueNumber', - 'valueBoolean', - 'valueMap', - ]; - for (final valueKey in valueKeys) { - if (item.containsKey(valueKey)) { - if (valueCount == 0) { - if (valueKey == 'valueMap') { - if (item[valueKey] is List) { - value = _parseDataModelContents( - (item[valueKey] as List).cast(), - ); - } else { - genUiLogger.warning( - 'valueMap for key "$key" is not a List: ${item[valueKey]}', - ); - } - } else { - value = item[valueKey]; - } - } - valueCount++; - } - } + source.addListener(onSourceChanged); + _externalSubscriptions.add(() => source.removeListener(onSourceChanged)); - if (valueCount == 0) { + if (twoWay) { + if (source is! ValueNotifier) { genUiLogger.warning( - 'No value field found for key "$key" in contents: $item', + 'bindExternalState: twoWay is true but source is not a ' + 'ValueNotifier.', ); - } else if (valueCount > 1) { - genUiLogger.warning( - 'Multiple value fields found for key "$key" in contents: $item. ' - 'Using the first one found.', + } else { + final ValueNotifier notifier = source; + final ValueNotifier subscription = subscribe(path); + + void onModelChanged() { + final T? modelValue = subscription.value; + if (modelValue != null && modelValue != notifier.value) { + notifier.value = modelValue; + } + } + + subscription.addListener(onModelChanged); + _externalSubscriptions.add( + () => subscription.removeListener(onModelChanged), ); } - newData[key] = value; } - return newData; + } + + /// Disposes resources and bindings. + void dispose() { + for (final VoidCallback callback in _cleanupCallbacks) { + callback(); + } + _cleanupCallbacks.clear(); + + for (final VoidCallback callback in _externalSubscriptions) { + callback(); + } + _externalSubscriptions.clear(); + + for (final ValueNotifier notifier in _subscriptions.values) { + notifier.dispose(); + } + _subscriptions.clear(); + } + + /// Retrieves a static, one-time value from the data model at the + /// specified absolute path without creating a subscription. + T? getValue(DataPath absolutePath) { + if (absolutePath == DataPath.root) { + return _data as T?; + } + return _getValue(_data, absolutePath.segments) as T?; } /// Retrieves a static, one-time value from the data model at the /// specified path segments without creating a subscription. - /// - /// The [current] parameter is the current node in the data model being - /// traversed. - /// The [segments] parameter is the list of remaining path segments to - /// traverse. Object? _getValue(Object? current, List segments) { if (segments.isEmpty) { return current; @@ -276,12 +375,6 @@ class DataModel { } /// Updates the given path with a new value without creating a subscription. - /// - /// The [current] parameter is the current node in the data model being - /// traversed. - /// The [segments] parameter is the list of remaining path segments to - /// traverse. - /// The [value] parameter is the new value to set at the specified path. void _updateValue(Object? current, List segments, Object? value) { if (segments.isEmpty) { return; @@ -292,19 +385,22 @@ class DataModel { if (current is Map) { if (remaining.isEmpty) { - current[segment] = value; + if (value == null) { + current.remove(segment); + } else { + current[segment] = value; + } return; } - // If we are here, remaining is not empty. Object? nextNode = current[segment]; if (nextNode == null) { - // Create the node if it doesn't exist, so the recursive call can - // populate it. + if (value == null) { + return; + } + final String nextSegment = remaining.first; - final bool isNextSegmentListIndex = nextSegment.startsWith( - RegExp(r'^\d+$'), - ); + final isNextSegmentListIndex = int.tryParse(nextSegment) != null; nextNode = isNextSegmentListIndex ? [] : {}; current[segment] = nextNode; } @@ -314,59 +410,50 @@ class DataModel { if (index != null && index >= 0) { if (remaining.isEmpty) { if (index < current.length) { - current[index] = value; + if (value == null) { + current[index] = value; + } else { + current[index] = value; + } } else if (index == current.length) { - current.add(value); - } else { - throw ArgumentError( - 'Index out of bounds for list update: index ($index) is greater ' - 'than list length (${current.length}).', - ); + if (value != null) current.add(value); } } else { if (index < current.length) { _updateValue(current[index], remaining, value); } else if (index == current.length) { - // If the index is the length, we're adding a new item which - // should be a map or list based on the next segment. - if (remaining.first.startsWith(RegExp(r'^\d+$'))) { - current.add([]); - } else { - current.add({}); - } - _updateValue(current[index], remaining, value); - } else { - throw ArgumentError( - 'Index out of bounds for nested update: index ($index) is ' - 'greater than list length (${current.length}).', - ); + final String nextSegment = remaining.first; + final isNextSegmentListIndex = int.tryParse(nextSegment) != null; + final Object newItem = isNextSegmentListIndex + ? [] + : {}; + current.add(newItem); + _updateValue(newItem, remaining, value); } } - } else { - genUiLogger.warning('Invalid list index segment: $segment'); } } } void _notifySubscribers(DataPath path) { - genUiLogger.info( - 'DataModel._notifySubscribers: notifying ' - '${_subscriptions.length} subscribers for path=$path', - ); - for (final DataPath p in _subscriptions.keys) { - if (p.startsWith(path) || path.startsWith(p)) { - genUiLogger.info(' - Notifying subscriber for path=$p'); - final ValueNotifier? subscriber = _subscriptions[p]; - if (subscriber != null) { - subscriber.value = getValue(p); - } + if (_subscriptions.containsKey(path)) { + _subscriptions[path]!.value = getValue(path); + } + + var parent = path; + while (!parent.isAbsolute || parent.segments.isNotEmpty) { + if (parent == DataPath.root) break; + parent = parent.dirname; + if (_subscriptions.containsKey(parent)) { + _subscriptions[parent]!.value = getValue(parent); } } - if (_valueSubscriptions.containsKey(path)) { - genUiLogger.info(' - Notifying value subscriber for path=$path'); - final ValueNotifier? subscriber = _valueSubscriptions[path]; - if (subscriber != null) { - subscriber.value = getValue(path); + if (path != DataPath.root && _subscriptions.containsKey(DataPath.root)) { + _subscriptions[DataPath.root]!.value = getValue(DataPath.root); + } + for (final DataPath p in _subscriptions.keys) { + if (p.startsWith(path) && p != path) { + _subscriptions[p]!.value = getValue(p); } } } diff --git a/packages/genui/lib/src/model/generation_events.dart b/packages/genui/lib/src/model/generation_events.dart new file mode 100644 index 000000000..74c6bd00a --- /dev/null +++ b/packages/genui/lib/src/model/generation_events.dart @@ -0,0 +1,67 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'a2ui_message.dart'; + +/// A base class for events related to the GenUI generation process. +sealed class GenerationEvent { + const GenerationEvent(); +} + +/// Fired when a tool execution starts. +class ToolStartEvent extends GenerationEvent { + const ToolStartEvent({required this.toolName, required this.args}); + + final String toolName; + final Map args; +} + +/// Fired when a tool execution completes. +class ToolEndEvent extends GenerationEvent { + const ToolEndEvent({ + required this.toolName, + required this.result, + required this.duration, + }); + + final String toolName; + final Object? result; + final Duration duration; +} + +/// Fired to report token usage. +class TokenUsageEvent extends GenerationEvent { + const TokenUsageEvent({ + required this.inputTokens, + required this.outputTokens, + }); + + final int inputTokens; + final int outputTokens; +} + +/// Fired when the AI emits a "thinking" chunk (if supported). +class ThinkingEvent extends GenerationEvent { + const ThinkingEvent({required this.content}); + + final String content; +} + +/// An event containing a text chunk from the LLM. +class TextEvent extends GenerationEvent { + /// Creates a [TextEvent] with the given [text]. + const TextEvent(this.text); + + /// The text content. + final String text; +} + +/// An event containing a parsed [A2uiMessage]. +class A2uiMessageEvent extends GenerationEvent { + /// Creates an [A2uiMessageEvent] with the given [message]. + const A2uiMessageEvent(this.message); + + /// The parsed message. + final A2uiMessage message; +} diff --git a/packages/genui/lib/src/model/parts.dart b/packages/genui/lib/src/model/parts.dart index a65a81e0c..b4a0a44b4 100644 --- a/packages/genui/lib/src/model/parts.dart +++ b/packages/genui/lib/src/model/parts.dart @@ -2,19 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:genai_primitives/genai_primitives.dart'; - -import 'parts/image.dart'; -import 'parts/ui.dart'; -export 'parts/image.dart'; export 'parts/ui.dart'; - -final _genuiPartConverterRegistry = { - ImagePart.type: const PartConverter(ImagePart.fromJson), - UiInteractionPart.type: const PartConverter(UiInteractionPart.fromJson), - UiPart.type: const PartConverter(UiPart.fromJson), - ...defaultPartConverterRegistry, -}; - -Parts genuiPartsFromJson(List json) => - Parts.fromJson(json, converterRegistry: _genuiPartConverterRegistry); diff --git a/packages/genui/lib/src/model/parts/image.dart b/packages/genui/lib/src/model/parts/image.dart deleted file mode 100644 index 696a11975..000000000 --- a/packages/genui/lib/src/model/parts/image.dart +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:genai_primitives/genai_primitives.dart'; - -final class _Json { - static const mimeType = 'mimeType'; - static const bytes = 'bytes'; - static const base64 = 'base64'; - static const url = 'url'; -} - -/// An image part of a message. -/// -/// Use the factory constructors to create an instance from different sources. -final class ImagePart extends Part { - static const String type = 'Image'; - - /// The raw image bytes. May be null if created from a URL or Base64. - final Uint8List? bytes; - - /// The Base64 encoded image string. May be null if created from bytes or URL. - final String? base64; - - /// The URL of the image. May be null if created from bytes or Base64. - final Uri? url; - - /// The MIME type of the image (e.g., 'image/jpeg', 'image/png'). - /// Required when providing image data directly. - final String mimeType; - - // Private constructor to enforce creation via factories. - const ImagePart._({ - this.bytes, - this.base64, - this.url, - required this.mimeType, - }); - - /// Creates an [ImagePart] from raw image bytes. - const factory ImagePart.fromBytes( - Uint8List bytes, { - required String mimeType, - }) = _ImagePartFromBytes; - - /// Creates an [ImagePart] from a Base64 encoded string. - const factory ImagePart.fromBase64( - String base64, { - required String mimeType, - }) = _ImagePartFromBase64; - - /// Creates an [ImagePart] from a URL. - const factory ImagePart.fromUrl(Uri url, {required String mimeType}) = - _ImagePartFromUrl; - - /// Creates an image part from a JSON map. - factory ImagePart.fromJson(Map json) { - if (json.containsKey(_Json.bytes)) { - return ImagePart.fromBytes( - Uint8List.fromList((json[_Json.bytes] as List).cast()), - mimeType: json[_Json.mimeType] as String, - ); - } else if (json.containsKey(_Json.base64)) { - return ImagePart.fromBase64( - json[_Json.base64] as String, - mimeType: json[_Json.mimeType] as String, - ); - } else if (json.containsKey(_Json.url)) { - final Object? urlValue = json[_Json.url]; - final Uri uri; - uri = Uri.parse(urlValue as String); - - return ImagePart.fromUrl(uri, mimeType: json[_Json.mimeType] as String); - } - throw FormatException('Invalid JSON for ImagePart: $json'); - } - - @override - Map toJson() => { - Part.typeKey: type, - _Json.mimeType: mimeType, - if (bytes != null) _Json.bytes: bytes, - if (base64 != null) _Json.base64: base64, - if (url != null) _Json.url: url.toString(), - }; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - return other is ImagePart && - other.mimeType == mimeType && - other.base64 == base64 && - other.url == url && - const DeepCollectionEquality().equals(other.bytes, bytes); - } - - @override - int get hashCode => Object.hash( - mimeType, - base64, - url, - const DeepCollectionEquality().hash(bytes), - ); -} - -// Private implementation classes for ImagePart factories -final class _ImagePartFromBytes extends ImagePart { - const _ImagePartFromBytes(Uint8List bytes, {required super.mimeType}) - : super._(bytes: bytes); -} - -final class _ImagePartFromBase64 extends ImagePart { - const _ImagePartFromBase64(String base64, {required super.mimeType}) - : super._(base64: base64); -} - -final class _ImagePartFromUrl extends ImagePart { - const _ImagePartFromUrl(Uri url, {required super.mimeType}) - : super._(url: url); -} diff --git a/packages/genui/lib/src/model/parts/ui.dart b/packages/genui/lib/src/model/parts/ui.dart index 83a2645f6..e291dc808 100644 --- a/packages/genui/lib/src/model/parts/ui.dart +++ b/packages/genui/lib/src/model/parts/ui.dart @@ -2,8 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:convert'; + import 'package:flutter/widgets.dart'; import 'package:genai_primitives/genai_primitives.dart'; + import '../../primitives/simple_items.dart'; import '../ui_models.dart'; @@ -13,87 +16,116 @@ final class _Json { static const interaction = 'interaction'; } -/// A part representing a UI definition to be rendered. -@immutable -final class UiPart extends Part { - static const type = 'Ui'; +/// Constants for UI related parts. +abstract final class UiPartConstants { + /// MIME type for UI definition parts. + static const uiMimeType = 'application/vnd.genui.ui+json'; - /// Creates a UI part. - UiPart({required this.definition, String? surfaceId}) - : surfaceId = surfaceId ?? generateId(), - uiKey = UniqueKey(); + /// MIME type for UI interaction parts. + static const interactionMimeType = 'application/vnd.genui.interaction+json'; +} - /// The JSON definition of the UI. - final UiDefinition definition; +/// Helper extension to interact with UI parts. +extension UiPartExtension on StandardPart { + /// Whether this part is a UI part. + bool get isUiPart => + this is DataPart && + (this as DataPart).mimeType == UiPartConstants.uiMimeType; + + /// Whether this part is a UI interaction part. + bool get isUiInteractionPart => + this is DataPart && + (this as DataPart).mimeType == UiPartConstants.interactionMimeType; + + /// Returns this part as a [UiPart] view, if generic type checks out. + /// + /// Functionally equivalent to parsing the [DataPart]. + UiPart? get asUiPart { + if (!isUiPart) return null; + return UiPart.fromDataPart(this as DataPart); + } - /// The unique ID for this UI surface. - final String surfaceId; + /// Returns this part as a [UiInteractionPart] view. + UiInteractionPart? get asUiInteractionPart { + if (!isUiInteractionPart) return null; + return UiInteractionPart.fromDataPart(this as DataPart); + } +} - /// A unique key for the UI widget. - final Key uiKey; +extension UiPartListExtension on Iterable { + /// Filters the list for UI parts and returns them as [UiPart] views. + Iterable get uiParts => + where((p) => p.isUiPart).map((p) => p.asUiPart!); + + /// Filters the list for UI interaction parts. + Iterable get uiInteractionParts => + where((p) => p.isUiInteractionPart).map((p) => p.asUiInteractionPart!); +} - /// Creates a UI part from a JSON map. - factory UiPart.fromJson(Map json) { - return UiPart( - definition: UiDefinition.fromJson( +/// A view over a [DataPart] representing a UI definition. +@immutable +final class UiPart { + /// Creates a [DataPart] compatible with GenUI. + static DataPart create({ + required SurfaceDefinition definition, + String? surfaceId, + }) { + final Map json = { + _Json.definition: definition.toJson(), + _Json.surfaceId: surfaceId ?? generateId(), + }; + return DataPart( + utf8.encode(jsonEncode(json)), + mimeType: UiPartConstants.uiMimeType, + ); + } + + /// Creates a view from a [DataPart]. + factory UiPart.fromDataPart(DataPart part) { + if (part.mimeType != UiPartConstants.uiMimeType) { + throw ArgumentError('Part is not a UI part'); + } + final json = jsonDecode(utf8.decode(part.bytes)) as Map; + return UiPart._( + definition: SurfaceDefinition.fromJson( json[_Json.definition] as Map, ), surfaceId: json[_Json.surfaceId] as String?, ); } - @override - Map toJson() => { - Part.typeKey: type, - _Json.definition: definition.toJson(), - _Json.surfaceId: surfaceId, - }; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - return other is UiPart && - other.definition == definition && - other.surfaceId == surfaceId; - } + const UiPart._({required this.definition, required this.surfaceId}); + + /// The JSON definition of the UI. + final SurfaceDefinition definition; - @override - int get hashCode => Object.hash(definition, surfaceId); + /// The unique ID for this UI surface. + final String? surfaceId; } -/// A part representing a user's interaction with the UI. +/// A view over a [DataPart] representing a UI interaction. @immutable -final class UiInteractionPart extends Part { - static const type = 'UiInteraction'; - - /// Creates a UI interaction part. - const UiInteractionPart(this.interaction); - - /// The interaction data (JSON string). - final String interaction; - - /// Creates a UI interaction part from a JSON map. - factory UiInteractionPart.fromJson(Map json) { - return UiInteractionPart(json[_Json.interaction] as String); +final class UiInteractionPart { + /// Creates a [DataPart] representing a UI interaction. + static DataPart create(String interaction) { + final Map json = {_Json.interaction: interaction}; + return DataPart( + utf8.encode(jsonEncode(json)), + mimeType: UiPartConstants.interactionMimeType, + ); } - @override - Map toJson() => { - Part.typeKey: type, - _Json.interaction: interaction, - }; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - return other is UiInteractionPart && other.interaction == interaction; + /// Creates a view from a [DataPart]. + factory UiInteractionPart.fromDataPart(DataPart part) { + if (part.mimeType != UiPartConstants.interactionMimeType) { + throw ArgumentError('Part is not a UI interaction part'); + } + final json = jsonDecode(utf8.decode(part.bytes)) as Map; + return UiInteractionPart._(json[_Json.interaction] as String); } - @override - int get hashCode => interaction.hashCode; + const UiInteractionPart._(this.interaction); - @override - String toString() => 'UiInteractionPart(interaction: $interaction)'; + /// The interaction data (JSON string). + final String interaction; } diff --git a/packages/genui/lib/src/model/ui_models.dart b/packages/genui/lib/src/model/ui_models.dart index 93f655e69..a0db0391f 100644 --- a/packages/genui/lib/src/model/ui_models.dart +++ b/packages/genui/lib/src/model/ui_models.dart @@ -5,8 +5,8 @@ import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; -import '../model/tools.dart'; import '../primitives/simple_items.dart'; /// A callback that is called when events are sent. @@ -18,7 +18,7 @@ typedef DispatchEventCallback = void Function(UiEvent event); /// A data object that represents a user interaction event in the UI. /// -/// This is used to send information from the app to the AI about user +/// Used to send information from the app to the AI about user /// actions, such as tapping a button or entering text. extension type UiEvent.fromMap(JsonMap _json) { /// The ID of the surface that this event originated from. @@ -30,15 +30,9 @@ extension type UiEvent.fromMap(JsonMap _json) { /// The type of event that was triggered (e.g., 'onChanged', 'onTap'). String get eventType => _json['eventType'] as String; - /// Whether this event should trigger an event. + /// The value associated with the event, if any. /// - /// The event can be a submission to the AI or - /// a change in the UI state that should be handled by - /// host of the surface. - bool get isAction => _json['isAction'] as bool; - - /// The value associated with the event, if any (e.g., the text in a - /// `TextField`, or the value of a `Checkbox`). + /// For example, the text in a `TextField`, or the value of a `Checkbox`. Object? get value => _json['value']; /// The timestamp of when the event occurred. @@ -50,8 +44,7 @@ extension type UiEvent.fromMap(JsonMap _json) { /// A UI event that represents a user action. /// -/// This is used for events that should trigger a submission to the AI, such as -/// tapping a button. +/// Triggers a submission to the AI, such as tapping a button. extension type UserActionEvent.fromMap(JsonMap _json) implements UiEvent { /// Creates a [UserActionEvent] from a set of properties. UserActionEvent({ @@ -65,32 +58,32 @@ extension type UserActionEvent.fromMap(JsonMap _json) implements UiEvent { 'name': name, 'sourceComponentId': sourceComponentId, 'timestamp': (timestamp ?? DateTime.now()).toIso8601String(), - 'isAction': true, 'context': context ?? {}, }; + /// The name of the action. String get name => _json['name'] as String; + + /// The ID of the component that triggered the action. String get sourceComponentId => _json['sourceComponentId'] as String; + + /// Context associated with the action. JsonMap get context => _json['context'] as JsonMap; } final class _Json { - static const String rootComponentId = 'rootComponentId'; static const String catalogId = 'catalogId'; static const String components = 'components'; - static const String styles = 'styles'; + static const String theme = 'theme'; } /// A data object that represents the entire UI definition. /// -/// This is the root object that defines a complete UI to be rendered. -class UiDefinition { +/// The root object that defines a complete UI to be rendered. +class SurfaceDefinition { /// The ID of the surface that this UI belongs to. final String surfaceId; - /// The ID of the root widget in the UI tree. - final String? rootComponentId; - /// The ID of the catalog to use for rendering this surface. final String? catalogId; @@ -98,46 +91,42 @@ class UiDefinition { Map get components => UnmodifiableMapView(_components); final Map _components; - /// (Future) The styles for this surface. - final JsonMap? styles; + /// The theme for this surface. + final JsonMap? theme; - /// Creates a [UiDefinition]. - UiDefinition({ + /// Creates a [SurfaceDefinition]. + SurfaceDefinition({ required this.surfaceId, - this.rootComponentId, this.catalogId, Map components = const {}, - this.styles, + this.theme, }) : _components = components; - /// Creates a [UiDefinition] from a JSON map. - factory UiDefinition.fromJson(JsonMap json) { - return UiDefinition( + /// Creates a [SurfaceDefinition] from a JSON map. + factory SurfaceDefinition.fromJson(JsonMap json) { + return SurfaceDefinition( surfaceId: json[surfaceIdKey] as String, - rootComponentId: json[_Json.rootComponentId] as String?, catalogId: json[_Json.catalogId] as String?, components: (json[_Json.components] as Map?)?.map( (key, value) => MapEntry(key, Component.fromJson(value as JsonMap)), ) ?? const {}, - styles: json[_Json.styles] as JsonMap?, + theme: json[_Json.theme] as JsonMap?, ); } - /// Creates a copy of this [UiDefinition] with the given fields replaced. - UiDefinition copyWith({ - String? rootComponentId, + /// Creates a copy of this [SurfaceDefinition] with the given fields replaced. + SurfaceDefinition copyWith({ String? catalogId, Map? components, - JsonMap? styles, + JsonMap? theme, }) { - return UiDefinition( + return SurfaceDefinition( surfaceId: surfaceId, - rootComponentId: rootComponentId ?? this.rootComponentId, catalogId: catalogId ?? this.catalogId, components: components ?? _components, - styles: styles ?? this.styles, + theme: theme ?? this.theme, ); } @@ -145,20 +134,189 @@ class UiDefinition { JsonMap toJson() { return { surfaceIdKey: surfaceId, - if (rootComponentId != null) _Json.rootComponentId: rootComponentId, if (catalogId != null) _Json.catalogId: catalogId, _Json.components: components.map( (key, value) => MapEntry(key, value.toJson()), ), - if (styles != null) _Json.styles: styles, + if (theme != null) _Json.theme: theme, }; } - /// Converts a UI definition into a blob of text + /// Converts a UI definition into a blob of text. String asContextDescriptionText() { final String text = jsonEncode(this); return 'A user interface is shown with the following content:\n$text.'; } + + /// Validates the UI definition against a schema. + /// + /// Throws [A2uiValidationException] if validation fails. + void validate(Schema schema) { + final String jsonOutput = schema.toJson(); + final schemaMap = jsonDecode(jsonOutput) as Map; + + List> allowedSchemas = []; + if (schemaMap.containsKey('oneOf')) { + allowedSchemas = (schemaMap['oneOf'] as List) + .cast>(); + } else if (schemaMap.containsKey('properties') && + (schemaMap['properties'] as Map).containsKey('components')) { + final componentsProp = + (schemaMap['properties'] as Map)['components'] + as Map; + if (componentsProp.containsKey('items')) { + final items = componentsProp['items'] as Map; + if (items.containsKey('oneOf')) { + allowedSchemas = (items['oneOf'] as List) + .cast>(); + } else { + allowedSchemas = [items]; + } + } else if (componentsProp.containsKey('properties')) { + allowedSchemas = (componentsProp['properties'] as Map).values + .cast>() + .toList(); + } + } + + if (allowedSchemas.isEmpty) { + return; + } + + for (final Component component in components.values) { + var matched = false; + List errors = []; + final JsonMap instanceJson = component.toJson(); + + for (final s in allowedSchemas) { + if (_schemaMatchesType(s, component.type)) { + try { + _validateInstance(instanceJson, s, '/components/${component.id}'); + matched = true; + break; + } catch (e) { + errors.add(e.toString()); + } + } + } + + if (!matched) { + if (errors.isNotEmpty) { + throw A2uiValidationException( + 'Validation failed for component ${component.id} ' + '(${component.type}): ${errors.join("; ")}', + surfaceId: surfaceId, + path: '/components/${component.id}', + ); + } + throw A2uiValidationException( + 'Unknown component type: ${component.type}', + surfaceId: surfaceId, + path: '/components/${component.id}', + ); + } + } + } + + bool _schemaMatchesType(Map schema, String type) { + if (schema.containsKey('properties')) { + final props = schema['properties'] as Map; + if (props.containsKey('component')) { + final compProp = props['component'] as Map; + if (compProp.containsKey('const') && compProp['const'] == type) { + return true; + } + if (compProp.containsKey('enum') && + (compProp['enum'] as List).contains(type)) { + return true; + } + } + } + return false; + } + + void _validateInstance( + Object? instance, + Map schema, + String path, + ) { + if (instance == null) { + return; + } + + if (schema.containsKey('const')) { + final Object? constVal = schema['const']; + if (instance != constVal) { + throw A2uiValidationException( + 'Value mismatch. Expected $constVal, got $instance', + surfaceId: surfaceId, + path: path, + ); + } + } + + if (schema.containsKey('enum')) { + final enums = schema['enum'] as List; + if (!enums.contains(instance)) { + throw A2uiValidationException( + 'Value not in enum: $instance', + surfaceId: surfaceId, + path: path, + ); + } + } + + if (schema.containsKey('required') && instance is Map) { + final List required = (schema['required'] as List).cast(); + for (final key in required) { + if (!instance.containsKey(key)) { + throw A2uiValidationException( + 'Missing required property: $key', + surfaceId: surfaceId, + path: path, + ); + } + } + } + + if (schema.containsKey('properties') && instance is Map) { + final props = schema['properties'] as Map; + for (final MapEntry entry in props.entries) { + final String key = entry.key; + final propSchema = entry.value as Map; + if (instance.containsKey(key)) { + _validateInstance(instance[key], propSchema, '$path/$key'); + } + } + } + + if (schema.containsKey('items') && instance is List) { + final itemsSchema = schema['items'] as Map; + for (var i = 0; i < instance.length; i++) { + _validateInstance(instance[i], itemsSchema, '$path/$i'); + } + } + + if (schema.containsKey('oneOf')) { + final List> oneOfs = (schema['oneOf'] as List) + .cast>(); + var oneMatched = false; + for (final s in oneOfs) { + try { + _validateInstance(instance, s, path); + oneMatched = true; + break; + } catch (_) {} + } + if (!oneMatched) { + throw A2uiValidationException( + 'Value did not match any oneOf schema', + surfaceId: surfaceId, + path: path, + ); + } + } + } } /// A component in the UI. @@ -166,8 +324,8 @@ final class Component { /// Creates a [Component]. const Component({ required this.id, - required this.componentProperties, - this.weight, + required this.type, + required this.properties, }); /// Creates a [Component] from a JSON map. @@ -175,48 +333,112 @@ final class Component { if (json['component'] == null) { throw ArgumentError('Component.fromJson: component property is null'); } - return Component( - id: json['id'] as String, - componentProperties: json['component'] as JsonMap, - weight: json['weight'] as int?, - ); + final rawType = json['component'] as String; + final id = json['id'] as String; + + final properties = Map.from(json); + properties.remove('id'); + properties.remove('component'); + + return Component(id: id, type: rawType, properties: properties); } /// The unique ID of the component. final String id; - /// The properties of the component. - final JsonMap componentProperties; + /// The type of the component (e.g. 'Text', 'Button'). + final String type; - /// The weight of the component, used for layout in Row/Column. - final int? weight; + /// The properties of the component. + final JsonMap properties; /// Converts this object to a JSON map. JsonMap toJson() { - return { - 'id': id, - 'component': componentProperties, - if (weight != null) 'weight': weight, - }; + return {'id': id, 'component': type, ...properties}; } - /// The type of the component. - String get type => componentProperties.keys.first; - @override bool operator ==(Object other) => other is Component && id == other.id && - weight == other.weight && - const DeepCollectionEquality().equals( - componentProperties, - other.componentProperties, - ); + type == other.type && + const DeepCollectionEquality().equals(properties, other.properties); @override - int get hashCode => Object.hash( - id, - weight, - const DeepCollectionEquality().hash(componentProperties), - ); + int get hashCode => + Object.hash(id, type, const DeepCollectionEquality().hash(properties)); +} + +/// Exception thrown when validation fails. +class A2uiValidationException implements Exception { + /// Creates a [A2uiValidationException]. + A2uiValidationException( + this.message, { + this.surfaceId, + this.path, + this.json, + this.cause, + }); + + /// The error message. + final String message; + + /// The ID of the surface where the validation error occurred. + final String? surfaceId; + + /// The path in the data/component model where the error occurred. + final String? path; + + /// The JSON that caused the error. + final Object? json; + + /// The underlying cause of the error. + final Object? cause; + + @override + String toString() { + final buffer = StringBuffer('A2uiValidationException: $message'); + if (surfaceId != null) buffer.write(' (surface: $surfaceId)'); + if (path != null) buffer.write(' (path: $path)'); + if (cause != null) buffer.write('\nCause: $cause'); + if (json != null) buffer.write('\nJSON: $json'); + return buffer.toString(); + } +} + +/// A sealed class representing an update to the UI managed by the system. +/// +/// Subclasses: [SurfaceAdded], [ComponentsUpdated], and [SurfaceRemoved]. +sealed class SurfaceUpdate { + /// Creates a [SurfaceUpdate] for the given [surfaceId]. + const SurfaceUpdate(this.surfaceId); + + /// The ID of the surface that was updated. + final String surfaceId; +} + +/// Fired when a new surface is created. +final class SurfaceAdded extends SurfaceUpdate { + /// Creates a [SurfaceAdded] event for the given [surfaceId] and + /// [definition]. + const SurfaceAdded(super.surfaceId, this.definition); + + /// The definition of the new surface. + final SurfaceDefinition definition; +} + +/// Fired when an existing surface is modified. +final class ComponentsUpdated extends SurfaceUpdate { + /// Creates a [ComponentsUpdated] event for the given [surfaceId] and + /// [definition]. + const ComponentsUpdated(super.surfaceId, this.definition); + + /// The new definition of the surface. + final SurfaceDefinition definition; +} + +/// Fired when a surface is deleted. +final class SurfaceRemoved extends SurfaceUpdate { + /// Creates a [SurfaceRemoved] event for the given [surfaceId]. + const SurfaceRemoved(super.surfaceId); } diff --git a/packages/genui/lib/src/primitives/cancellation.dart b/packages/genui/lib/src/primitives/cancellation.dart new file mode 100644 index 000000000..4c50c4bdb --- /dev/null +++ b/packages/genui/lib/src/primitives/cancellation.dart @@ -0,0 +1,50 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A signal that can be used to cancel an operation. +class CancellationSignal { + bool _isCancelled = false; + + final _listeners = []; + + /// Whether the operation has been cancelled. + bool get isCancelled => _isCancelled; + + /// Cancels the operation. + void cancel() { + if (_isCancelled) return; + _isCancelled = true; + for (final void Function() listener in _listeners) { + listener(); + } + } + + /// Adds a listener to be notified when the operation is cancelled. + void addListener(void Function() listener) { + if (_isCancelled) { + listener(); + } else { + _listeners.add(listener); + } + } + + /// Removes a listener. + void removeListener(void Function() listener) { + _listeners.remove(listener); + } +} + +/// An exception thrown when an operation is cancelled. +class CancellationException implements Exception { + /// Creates a [CancellationException]. + const CancellationException([this.message]); + + /// A message describing the cancellation. + final String? message; + + @override + String toString() => message == null + ? 'CancellationException' + : 'CancellationException: $message'; +} diff --git a/packages/genui/lib/src/primitives/constants.dart b/packages/genui/lib/src/primitives/constants.dart index c826615b6..b896a85c0 100644 --- a/packages/genui/lib/src/primitives/constants.dart +++ b/packages/genui/lib/src/primitives/constants.dart @@ -2,5 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/// The catalog ID for the standard catalog. -const String standardCatalogId = 'a2ui.org:standard_catalog_0_8_0'; +/// The catalog ID for the basic catalog. +const String basicCatalogId = + 'https://a2ui.org/specification/v0_9/standard_catalog.json'; diff --git a/packages/genui/lib/src/primitives/logging.dart b/packages/genui/lib/src/primitives/logging.dart index 2583dbca3..0e163240d 100644 --- a/packages/genui/lib/src/primitives/logging.dart +++ b/packages/genui/lib/src/primitives/logging.dart @@ -2,38 +2,47 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:logging/logging.dart'; /// The logger for the GenUI package. final genUiLogger = Logger('GenUI'); +StreamSubscription? _loggingSubscription; + /// Configures the logging for the GenUI package. /// /// This function should be called by applications using the GenUI package to /// configure the desired log level and to listen for log messages. -Logger configureGenUiLogging({ +/// +/// If [enableHierarchicalLogging] is true (the default), this function will set +/// [hierarchicalLoggingEnabled] to true on the [Logger] class. +Logger configureLogging({ Level level = Level.INFO, void Function(Level, String)? logCallback, + bool enableHierarchicalLogging = true, }) { logCallback ??= (level, message) { // ignore: avoid_print print(message); }; - hierarchicalLoggingEnabled = true; + if (enableHierarchicalLogging) { + hierarchicalLoggingEnabled = true; + } recordStackTraceAtLevel = Level.SEVERE; genUiLogger.level = level; - Logger.root.onRecord.listen((record) { - if (record.loggerName == genUiLogger.name) { - logCallback?.call( - record.level, - '[${record.level.name}] ${record.time}: ${record.message}', - ); - if (record.error != null) { - logCallback?.call(record.level, ' Error: ${record.error}'); - } - if (record.stackTrace != null) { - logCallback?.call(record.level, ' Stack trace:\n${record.stackTrace}'); - } + _loggingSubscription?.cancel(); + _loggingSubscription = genUiLogger.onRecord.listen((record) { + logCallback?.call( + record.level, + '[${record.level.name}] ${record.time}: ${record.message}', + ); + if (record.error != null) { + logCallback?.call(record.level, ' Error: ${record.error}'); + } + if (record.stackTrace != null) { + logCallback?.call(record.level, ' Stack trace:\n${record.stackTrace}'); } }); diff --git a/packages/genui/lib/src/primitives/simple_items.dart b/packages/genui/lib/src/primitives/simple_items.dart index c4a219fb0..5b7422829 100644 --- a/packages/genui/lib/src/primitives/simple_items.dart +++ b/packages/genui/lib/src/primitives/simple_items.dart @@ -4,6 +4,11 @@ import 'package:uuid/uuid.dart'; +/// A map of key-value pairs representing a JSON object. typedef JsonMap = Map; +/// Key used in schema definition to specify the component ID. +const String surfaceIdKey = 'surfaceId'; + +/// Generates a unique ID (UUID v4). String generateId() => const Uuid().v4(); diff --git a/packages/genui/lib/src/transport/a2ui_parser_transformer.dart b/packages/genui/lib/src/transport/a2ui_parser_transformer.dart new file mode 100644 index 000000000..fb87925c5 --- /dev/null +++ b/packages/genui/lib/src/transport/a2ui_parser_transformer.dart @@ -0,0 +1,242 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import '../model/a2ui_message.dart'; +import '../model/generation_events.dart'; +import '../model/ui_models.dart'; + +/// Transforms a stream of text chunks into a stream of logical +/// [GenerationEvent]s. +/// +/// It handles buffering split tokens, extracting JSON blocks, and sanitizing +/// text. +class A2uiParserTransformer + extends StreamTransformerBase { + /// Creating a const constructor for the transformer. + const A2uiParserTransformer(); + + @override + Stream bind(Stream stream) { + return _A2uiParserStream(stream).stream; + } +} + +class _A2uiParserStream { + _A2uiParserStream(Stream input) { + _controller = StreamController( + onListen: () { + _subscription = input.listen( + _onData, + onError: _controller.addError, + onDone: _onDone, + cancelOnError: false, + ); + }, + onPause: () => _subscription?.pause(), + onResume: () => _subscription?.resume(), + onCancel: () => _subscription?.cancel(), + ); + } + + late final StreamController _controller; + StreamSubscription? _subscription; + String _buffer = ''; + + Stream get stream => _controller.stream; + + void _onData(String chunk) { + _buffer += chunk; + _processBuffer(); + } + + void _onDone() { + // If there's anything left in the buffer that looks like text, emit it. + if (_buffer.isNotEmpty) { + _emitText(_buffer); + _buffer = ''; + } + _controller.close(); + } + + void _processBuffer() { + while (_buffer.isNotEmpty) { + // 1. Check for Markdown JSON block + final _Match? markdownMatch = _findMarkdownJson(_buffer); + if (markdownMatch != null) { + try { + final Object? decoded = jsonDecode(markdownMatch.content); + if (decoded != null) { + _emitBefore(markdownMatch.start); + _emitMessage(decoded); + _buffer = _buffer.substring(markdownMatch.end); + continue; + } + } catch (_) { + // Invalid JSON in markdown block. Consumed as text implicitly if we + // advance? No, we didn't advance. We treat it as text. Fall through + // to 3? If we don't handle it here, `firstPotentialStart` might pick + // `markdownStart` again and emit it as text. So we successfully + // effectively skip "parsing as message". + } + } + + // 2. Check for Balanced JSON + final _Match? jsonMatch = _findBalancedJson(_buffer); + if (jsonMatch != null) { + // Prioritize markdown if it starts BEFORE the balanced JSON logic would + // pick it up? + if (markdownMatch != null && markdownMatch.start <= jsonMatch.start) { + // We already tried markdown and failed (otherwise we continued). + // Fall through. + } + + try { + final Object? decoded = jsonDecode(jsonMatch.content); + if (decoded != null) { + _emitBefore(jsonMatch.start); + _emitMessage(decoded); + _buffer = _buffer.substring(jsonMatch.end); + continue; + } + } catch (_) { + // Invalid JSON. + } + } + + // 3. Fallback / Wait logic + final int markdownStart = _buffer.indexOf('```'); + final int braceStart = _buffer.indexOf('{'); + + var firstPotentialStart = -1; + if (markdownStart != -1 && braceStart != -1) { + firstPotentialStart = markdownStart < braceStart + ? markdownStart + : braceStart; + } else if (markdownStart != -1) { + firstPotentialStart = markdownStart; + } else { + firstPotentialStart = braceStart; + } + + if (firstPotentialStart == -1) { + // No potential JSON start. Emit all. + if (_buffer.isNotEmpty) { + _emitText(_buffer); + _buffer = ''; + } + break; + } else { + // Found a potential start at `firstPotentialStart`. + // Emit text BEFORE it. + if (firstPotentialStart > 0) { + _emitText(_buffer.substring(0, firstPotentialStart)); + _buffer = _buffer.substring(firstPotentialStart); + } + // Now buffer starts with potential JSON. + // Since we already tried to parse and failed (if we are here), + // we must wait for more data. + break; + } + } + } + + void _emitBefore(int index) { + if (index > 0) { + _emitText(_buffer.substring(0, index)); + } + } + + void _emitText(String text) { + // Clean up protocol tags that might leak into text stream + final String cleanText = text + .replaceAll('', '') + .replaceAll('', ''); + + if (cleanText.isNotEmpty) { + _controller.add(TextEvent(cleanText)); + } + } + + void _emitMessage(Object json) { + if (json is Map) { + try { + _controller.add(A2uiMessageEvent(A2uiMessage.fromJson(json))); + } on A2uiValidationException catch (e) { + _controller.addError(e); + } catch (e) { + // Failed to parse A2UI message structure (e.g. invalid type + // discriminator) + _controller.add(TextEvent(jsonEncode(json))); + } + } else if (json is List) { + for (final Object? item in json) { + if (item is Map) { + try { + _controller.add(A2uiMessageEvent(A2uiMessage.fromJson(item))); + } on A2uiValidationException catch (e) { + _controller.addError(e); + } catch (_) { + _controller.add(TextEvent(jsonEncode(item))); + } + } + } + } + } + + _Match? _findMarkdownJson(String text) { + final regex = RegExp(r'```(?:json)?\s*([\s\S]*?)\s*```'); + final RegExpMatch? match = regex.firstMatch(text); + if (match != null) { + return _Match(match.start, match.end, match.group(1) ?? ''); + } + return null; + } + + _Match? _findBalancedJson(String input) { + if (!input.startsWith('{')) return null; + + var balance = 0; + var inString = false; + var isEscaped = false; + + for (var i = 0; i < input.length; i++) { + final String char = input[i]; + + if (isEscaped) { + isEscaped = false; + continue; + } + if (char == '\\') { + isEscaped = true; + continue; + } + if (char == '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (char == '{') { + balance++; + } else if (char == '}') { + balance--; + if (balance == 0) { + return _Match(0, i + 1, input.substring(0, i + 1)); + } + } + } + } + return null; + } +} + +class _Match { + _Match(this.start, this.end, this.content); + final int start; + final int end; + final String content; +} diff --git a/packages/genui/lib/src/transport/a2ui_transport_adapter.dart b/packages/genui/lib/src/transport/a2ui_transport_adapter.dart new file mode 100644 index 000000000..043776b5a --- /dev/null +++ b/packages/genui/lib/src/transport/a2ui_transport_adapter.dart @@ -0,0 +1,91 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import '../interfaces/transport.dart'; +import '../model/a2ui_message.dart'; +import '../model/chat_message.dart'; +import '../model/generation_events.dart'; + +import 'a2ui_parser_transformer.dart'; + +export '../model/generation_events.dart' + show A2uiMessageEvent, GenerationEvent, TextEvent; + +/// A manual sender callback. +typedef ManualSendCallback = Future Function(ChatMessage message); + +/// The primary high-level API for typical Flutter application development. +/// +/// It wraps the [A2uiParserTransformer] to provide an imperative, push-based +/// interface that is easier to integrate into imperative loops. +/// +/// Use [addChunk] to feed text chunks from an LLM. +/// Use [addMessage] to feed raw A2UI messages. +class A2uiTransportAdapter implements Transport { + /// Creates a [A2uiTransportAdapter]. + /// + /// The [onSend] callback is required if [sendRequest] will be called. + A2uiTransportAdapter({this.onSend}) { + _pipeline = _inputStream.stream + .transform(const A2uiParserTransformer()) + .asBroadcastStream(); + } + + /// The callback to invoke when [sendRequest] is called. + final ManualSendCallback? onSend; + + final StreamController _inputStream = StreamController(); + final StreamController _messageStream = + StreamController.broadcast(); + late final Stream _pipeline; + StreamSubscription? _pipelineSubscription; + + /// Feeds a chunk of text from the LLM to the controller. + /// + /// The controller buffers and parses this internally using the transformer. + void addChunk(String text) { + _pipelineSubscription ??= _pipeline.listen((event) { + if (event is A2uiMessageEvent) { + _messageStream.add(event.message); + } + }); + _inputStream.add(text); + } + + /// Feeds a raw A2UI message (e.g. from a tool output or separate channel). + void addMessage(A2uiMessage message) { + _messageStream.add(message); + } + + /// A stream of sanitizer text for the chat UI. + @override + Stream get incomingText => _pipeline + .where((e) => e is TextEvent) + .cast() + .map((e) => e.text); + + /// A stream of A2UI messages parsed from the input. + @override + Stream get incomingMessages => _messageStream.stream; + + @override + Future sendRequest(ChatMessage message) async { + if (onSend == null) { + throw StateError( + 'A2uiTransportAdapter.onSend must be provided to use sendRequest.', + ); + } + await onSend!(message); + } + + /// Closes the controller and cleans up resources. + @override + void dispose() { + _inputStream.close(); + _messageStream.close(); + _pipelineSubscription?.cancel(); + } +} diff --git a/packages/genui/lib/src/utils/json_block_parser.dart b/packages/genui/lib/src/utils/json_block_parser.dart new file mode 100644 index 000000000..e1bfba773 --- /dev/null +++ b/packages/genui/lib/src/utils/json_block_parser.dart @@ -0,0 +1,145 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +/// Utilities for parsing JSON blocks from text, commonly used when extracting +/// structured data from LLM text responses. +class JsonBlockParser { + /// Parses the first valid JSON object or array found in [text]. + /// + /// This method looks for JSON patterns, handling: + /// - Markdown code blocks (e.g., ```json ... ```) + /// - Raw JSON objects/arrays directly in text + /// + /// Returns `null` if no valid JSON is found. + static Object? parseFirstJsonBlock(String text) { + final String? markdownBlock = _extractMarkdownJson(text); + if (markdownBlock != null) { + try { + return jsonDecode(markdownBlock); + } on FormatException catch (_) {} + } + + final int firstBrace = text.indexOf('{'); + final int firstBracket = text.indexOf('['); + + var start = -1; + if (firstBrace != -1 && firstBracket != -1) { + start = firstBrace < firstBracket ? firstBrace : firstBracket; + } else if (firstBrace != -1) { + start = firstBrace; + } else if (firstBracket != -1) { + start = firstBracket; + } + + if (start == -1) return null; + + final String input = text.substring(start); + try { + return jsonDecode(input); + } on FormatException catch (_) { + final String? result = _extractBalancedJson(input); + if (result != null) { + try { + return jsonDecode(result); + } on FormatException catch (_) { + return null; + } + } + return null; + } + } + + static String? _extractMarkdownJson(String text) { + final regex = RegExp(r'```(?:json)?\s*([\s\S]*?)\s*```'); + final RegExpMatch? match = regex.firstMatch(text); + return match?.group(1); + } + + static String? _extractBalancedJson(String input) { + if (input.isEmpty) return null; + final String startChar = input[0]; + final String? endChar = startChar == '{' + ? '}' + : (startChar == '[' ? ']' : null); + if (endChar == null) return null; + + var balance = 0; + var inString = false; + var isEscaped = false; + + for (var i = 0; i < input.length; i++) { + final String char = input[i]; + + if (isEscaped) { + isEscaped = false; + continue; + } + + if (char == '\\') { + isEscaped = true; + continue; + } + + if (char == '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (char == startChar) { + balance++; + } else if (char == endChar) { + balance--; + if (balance == 0) { + return input.substring(0, i + 1); + } + } + } + } + return null; + } + + /// Parses all valid JSON objects or arrays found in [text]. + static List parseJsonBlocks(String text) { + final results = []; + + final markdownRegex = RegExp(r'```(?:json)?\s*([\s\S]*?)\s*```'); + final Iterable matches = markdownRegex.allMatches(text); + + for (final match in matches) { + final String? content = match.group(1); + if (content != null) { + try { + results.add(jsonDecode(content) as Object); + } on FormatException catch (_) {} + } + } + if (results.isNotEmpty) { + return results; + } + + final Object? firstBlock = parseFirstJsonBlock(text); + if (firstBlock != null) { + results.add(firstBlock); + } + + return results; + } + + /// Removes all found JSON blocks from the text. + static String stripJsonBlock(String text) { + final markdownRegex = RegExp(r'```(?:json)?\s*([\s\S]*?)\s*```'); + String processed = text.replaceAll(markdownRegex, ''); + if (processed.length == text.length) { + final String? jsonString = _extractBalancedJson(text); + if (jsonString != null) { + processed = text.replaceFirst(jsonString, ''); + } + } + + return processed.trim(); + } +} diff --git a/packages/genui/lib/src/widgets/fallback_widget.dart b/packages/genui/lib/src/widgets/fallback_widget.dart new file mode 100644 index 000000000..02ae0c5a7 --- /dev/null +++ b/packages/genui/lib/src/widgets/fallback_widget.dart @@ -0,0 +1,89 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// A widget that displays a fallback UI for error or loading states. +/// +/// This is typically used to handle errors during content generation or UI +/// rendering, or to show a loading indicator while waiting for content. +class FallbackWidget extends StatelessWidget { + /// Creates a [FallbackWidget] widget. + const FallbackWidget({ + super.key, + this.error, + this.stackTrace, + this.onRetry, + this.loadingMessage, + this.isLoading = false, + }); + + /// The error object, if any. + final Object? error; + + /// The stack trace associated with the error, if any. + final StackTrace? stackTrace; + + /// A callback to trigger a retry operation. + final VoidCallback? onRetry; + + /// A message to display while loading. + final String? loadingMessage; + + /// Whether the widget is in a loading state. + final bool isLoading; + + @override + Widget build(BuildContext context) { + if (isLoading) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + if (loadingMessage != null) ...[ + const SizedBox(height: 16), + Text(loadingMessage!), + ], + ], + ), + ); + } + + if (error != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + Text( + 'An error occurred', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + error.toString(), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (onRetry != null) ...[ + const SizedBox(height: 16), + FilledButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ], + ), + ), + ); + } + + return const SizedBox.shrink(); + } +} diff --git a/packages/genui/lib/src/widgets/surface.dart b/packages/genui/lib/src/widgets/surface.dart new file mode 100644 index 000000000..802e5e28f --- /dev/null +++ b/packages/genui/lib/src/widgets/surface.dart @@ -0,0 +1,270 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +import '../interfaces/surface_context.dart'; +import '../model/catalog.dart'; +import '../model/catalog_item.dart'; +import '../model/data_model.dart'; +import '../model/ui_models.dart'; +import '../primitives/constants.dart'; +import '../primitives/logging.dart'; +import '../primitives/simple_items.dart'; +import 'fallback_widget.dart'; + +/// A callback for when a user interacts with a widget. +typedef UiEventCallback = void Function(UiEvent event); + +/// A widget that renders a dynamic UI surface generated by the AI. +/// +/// This widget connects to a [SurfaceContext] and renders the UI defined by the +/// [SurfaceDefinition] for the bound surface. +class Surface extends StatefulWidget { + /// Creates a [Surface]. + const Surface({ + super.key, + required this.surfaceContext, + this.defaultBuilder, + this.actionDelegate = const DefaultActionDelegate(), + }); + + /// The context that holds the state of this surface. + final SurfaceContext surfaceContext; + + /// A builder for the widget to display when the surface has no definition. + final WidgetBuilder? defaultBuilder; + + /// The delegate that handles UI actions. + final ActionDelegate actionDelegate; + + @override + State createState() => _SurfaceState(); +} + +class _SurfaceState extends State { + @override + Widget build(BuildContext context) { + genUiLogger.fine( + 'Outer Building surface ${widget.surfaceContext.surfaceId}', + ); + return ValueListenableBuilder( + valueListenable: widget.surfaceContext.definition, + builder: (context, definition, child) { + genUiLogger.fine('Building surface ${widget.surfaceContext.surfaceId}'); + if (definition == null) { + genUiLogger.info( + 'Surface ${widget.surfaceContext.surfaceId} has no definition.', + ); + return widget.defaultBuilder?.call(context) ?? + const SizedBox.shrink(); + } + // Implicit root is "root". + const rootId = 'root'; + if (definition.components.isEmpty || + !definition.components.containsKey(rootId)) { + genUiLogger.warning( + 'Surface ${widget.surfaceContext.surfaceId} has no root component.', + ); + return const SizedBox.shrink(); + } + + final Catalog? catalog = _findCatalogForDefinition(definition); + if (catalog == null) { + final error = Exception( + 'Catalog with id "${definition.catalogId}" not found.', + ); + widget.surfaceContext.reportError(error, StackTrace.current); + return FallbackWidget(error: error); + } + + return _buildWidget( + definition, + catalog, + rootId, + DataContext(widget.surfaceContext.dataModel, '/'), + ); + }, + ); + } + + /// The main recursive build function. + /// It reads a widget definition and its current state from + /// `widget.definition` + /// and constructs the corresponding Flutter widget. + Widget _buildWidget( + SurfaceDefinition definition, + Catalog catalog, + String widgetId, + DataContext dataContext, + ) { + try { + Component? data = definition.components[widgetId]; + if (data == null) { + final error = Exception('Widget with id: $widgetId not found.'); + genUiLogger.severe(error.toString()); + widget.surfaceContext.reportError(error, StackTrace.current); + return FallbackWidget(error: error); + } + + final JsonMap widgetData = data.properties; + genUiLogger.finest('Building widget $widgetId'); + return catalog.buildWidget( + CatalogItemContext( + id: widgetId, + data: widgetData, + type: data.type, + buildChild: (String childId, [DataContext? childDataContext]) => + _buildWidget( + definition, + catalog, + childId, + childDataContext ?? dataContext, + ), + dispatchEvent: _dispatchEvent, + buildContext: context, + dataContext: dataContext, + getComponent: (String componentId) => + definition.components[componentId], + getCatalogItem: (String type) => + catalog.items.firstWhereOrNull((item) => item.name == type), + surfaceId: widget.surfaceContext.surfaceId, + ), + ); + } catch (exception, stackTrace) { + genUiLogger.severe( + 'Error building widget $widgetId', + exception, + stackTrace, + ); + widget.surfaceContext.reportError(exception, stackTrace); + return FallbackWidget(error: exception, stackTrace: stackTrace); + } + } + + void _dispatchEvent(UiEvent event) { + if (widget.actionDelegate.handleEvent( + context, + event, + widget.surfaceContext, + _findCatalogForDefinition, + _buildWidget, + )) { + return; + } + + // The event comes in without a surfaceId, which we add here. + final Map eventMap = { + ...event.toMap(), + surfaceIdKey: widget.surfaceContext.surfaceId, + }; + final UiEvent newEvent = event is UserActionEvent + ? UserActionEvent.fromMap(eventMap) + : UiEvent.fromMap(eventMap); + widget.surfaceContext.handleUiEvent(newEvent); + } + + Catalog? _findCatalogForDefinition(SurfaceDefinition definition) { + final String catalogId = definition.catalogId ?? basicCatalogId; + final Catalog? catalog = widget.surfaceContext.catalogs.firstWhereOrNull( + (c) => c.catalogId == catalogId, + ); + + if (catalog == null) { + genUiLogger.severe( + 'Catalog with id "$catalogId" not found for surface ' + '"${widget.surfaceContext.surfaceId}". Ensure the catalog is provided ' + 'to A2uiMessageProcessor. Available catalogs: ' + '${widget.surfaceContext.catalogs.map((c) => c.catalogId).join(', ')}.', + ); + } + return catalog; + } +} + +/// A delegate for handling UI actions in [Surface]. +/// +/// Implement this interface to provide custom handling for specific actions, +/// such as showing modals or navigating. +abstract interface class ActionDelegate { + /// Handles a [UiEvent]. + /// + /// Returns `true` if the event was handled, `false` otherwise. + /// + /// The [context] is the build context of the [Surface]. + /// The [genUiContext] provides access to the surface state. + /// The [findCatalog] function helps resolve the catalog for the current + /// definition. + /// The [buildWidget] function allows building widgets from the definition, + /// useful for rendering content inside modals or dialogs. + bool handleEvent( + BuildContext context, + UiEvent event, + SurfaceContext genUiContext, + Catalog? Function(SurfaceDefinition) findCatalog, + Widget Function(SurfaceDefinition, Catalog, String, DataContext) + buildWidget, + ); +} + +/// The default action delegate that handles standard actions like 'showModal'. +class DefaultActionDelegate implements ActionDelegate { + /// Creates a [DefaultActionDelegate]. + const DefaultActionDelegate(); + + @override + bool handleEvent( + BuildContext context, + UiEvent event, + SurfaceContext genUiContext, + Catalog? Function(SurfaceDefinition) findCatalog, + Widget Function(SurfaceDefinition, Catalog, String, DataContext) + buildWidget, + ) { + if (event is UserActionEvent && event.name == 'showModal') { + final SurfaceDefinition? definition = genUiContext.definition.value; + if (definition == null) return true; + + final Catalog? catalog = findCatalog(definition); + if (catalog == null) { + genUiLogger.severe( + 'Cannot show modal for surface "${genUiContext.surfaceId}" ' + 'because a catalog was not found.', + ); + return true; + } + + final modalId = event.context['modalId'] as String?; + if (modalId == null) { + genUiLogger.severe('Modal action missing "modalId" in context.'); + return true; + } + + final Component? modalComponent = definition.components[modalId]; + if (modalComponent == null) return true; + + // The 'content' property is expected to be a direct property of the + // Modal component. + final contentChildId = modalComponent.properties['content'] as String?; + + if (contentChildId == null) { + genUiLogger.severe('Modal component missing "content" property.'); + return true; + } + + showModalBottomSheet( + context: context, + builder: (context) => buildWidget( + definition, + catalog, + contentChildId, + DataContext(genUiContext.dataModel, '/'), + ), + ); + return true; + } + return false; + } +} diff --git a/packages/genui/lib/src/widgets/widget_utilities.dart b/packages/genui/lib/src/widgets/widget_utilities.dart new file mode 100644 index 000000000..49893f946 --- /dev/null +++ b/packages/genui/lib/src/widgets/widget_utilities.dart @@ -0,0 +1,185 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import '../functions/expression_parser.dart'; +import '../model/data_model.dart'; +import '../primitives/simple_items.dart'; + +/// A builder widget that simplifies handling of nullable `ValueListenable`s. +/// +/// This widget listens to a `ValueListenable` and rebuilds its child +/// whenever the value changes. If the value is `null`, it returns a +/// `SizedBox.shrink()`, effectively hiding the child. If the value is not +/// `null`, it calls the `builder` function with the non-nullable value. +class OptionalValueBuilder extends StatelessWidget { + /// The `ValueListenable` to listen to. + final ValueListenable listenable; + + /// The builder function to call when the value is not `null`. + final Widget Function(BuildContext context, T value) builder; + + /// Creates an `OptionalValueBuilder`. + const OptionalValueBuilder({ + super.key, + required this.listenable, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: listenable, + builder: (context, value, _) { + if (value == null) return const SizedBox.shrink(); + return builder(context, value); + }, + ); + } +} + +/// Extension methods for [DataContext] to simplify data binding. +extension DataContextExtensions on DataContext { + /// Subscribes to a string value, which can be a literal or a data-bound path. + /// + /// This method is robust against type mismatches in the data model. If the + /// underlying value is not a String, it will be converted using [toString]. + ValueNotifier subscribeToString(Object? value) { + if (value is Map && value.containsKey('path')) { + final ValueNotifier raw = subscribe( + value['path'] as String, + ); + + return _ToStringNotifier(raw); + } + if (value is String && !value.contains(r'${')) { + return ValueNotifier(value); + } + return subscribe(value); + } + + /// Subscribes to a boolean value, which can be a literal or a data-bound + /// path. + ValueNotifier subscribeToBool(Object? value) { + if (value is Map && value.containsKey('path')) { + final ValueNotifier raw = subscribe( + value['path'] as String, + ); + return _ToBoolNotifier(raw); + } + return subscribe(value); + } + + /// Subscribes to a list of objects, which can be a literal or a data-bound + /// path. + ValueNotifier?> subscribeToObjectArray(Object? value) { + return subscribe>(value); + } + + /// Subscribes to a number value, which can be a literal or a data-bound + /// path. + ValueNotifier subscribeToNumber(Object? value) { + if (value is Map && value.containsKey('path')) { + final ValueNotifier raw = subscribe( + value['path'] as String, + ); + return _ToNumberNotifier(raw); + } + return subscribe(value); + } +} + +class _ToStringNotifier extends ValueNotifier { + _ToStringNotifier(this._source) : super(_source.value?.toString()) { + _source.addListener(_update); + } + + final ValueNotifier _source; + + void _update() { + super.value = _source.value?.toString(); + } + + @override + void dispose() { + _source.removeListener(_update); + super.dispose(); + } +} + +class _ToBoolNotifier extends ValueNotifier { + _ToBoolNotifier(this._source) : super(_convert(_source.value)) { + _source.addListener(_update); + } + + final ValueNotifier _source; + + static bool? _convert(Object? value) { + if (value is bool) return value; + if (value is String) { + if (value.toLowerCase() == 'true') return true; + if (value.toLowerCase() == 'false') return false; + } + if (value is num) return value != 0; + return null; + } + + void _update() { + super.value = _convert(_source.value); + } + + @override + void dispose() { + _source.removeListener(_update); + super.dispose(); + } +} + +class _ToNumberNotifier extends ValueNotifier { + _ToNumberNotifier(this._source) : super(_convert(_source.value)) { + _source.addListener(_update); + } + + final ValueNotifier _source; + + static num? _convert(Object? value) { + if (value is num) return value; + if (value is String) return num.tryParse(value); + return null; + } + + void _update() { + super.value = _convert(_source.value); + } + + @override + void dispose() { + _source.removeListener(_update); + super.dispose(); + } +} + +/// Resolves a context map definition against a [DataContext]. +/// +JsonMap resolveContext(DataContext dataContext, JsonMap? contextDefinition) { + final resolved = {}; + if (contextDefinition == null) return resolved; + + final parser = ExpressionParser(dataContext); + + for (final MapEntry entry in contextDefinition.entries) { + final String key = entry.key; + final Object? value = entry.value; + if (value is String) { + resolved[key] = parser.parse(value); + } else if (value is Map && value.containsKey('path')) { + resolved[key] = dataContext.getValue(value['path'] as String); + } else { + resolved[key] = value; + } + } + return resolved; +} diff --git a/packages/genui/lib/test.dart b/packages/genui/lib/test.dart index a59de76bd..c8b5a383f 100644 --- a/packages/genui/lib/test.dart +++ b/packages/genui/lib/test.dart @@ -2,5 +2,4 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -export 'test/fake_content_generator.dart'; export 'test/validation.dart'; diff --git a/packages/genui/lib/test/fake_content_generator.dart b/packages/genui/lib/test/fake_content_generator.dart deleted file mode 100644 index 372c9ce79..000000000 --- a/packages/genui/lib/test/fake_content_generator.dart +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import '../genui.dart'; - -/// A fake [ContentGenerator] for use in tests. -/// -/// This implementation allows tests to control AI responses by: -/// - Tracking calls to [sendRequest] via [sendRequestCallCount] -/// - Capturing the last message and history via [lastMessage] and [lastHistory] -/// - Emitting fake A2UI messages via [addA2uiMessage] -/// - Emitting fake text responses via [addTextResponse] -/// - Pausing execution via [sendRequestCompleter] -class FakeContentGenerator implements ContentGenerator { - /// Creates a new [FakeContentGenerator] instance. - FakeContentGenerator(); - - final _a2uiMessageController = StreamController.broadcast(); - final _textResponseController = StreamController.broadcast(); - final _errorController = StreamController.broadcast(); - final _isProcessing = ValueNotifier(false); - - /// A completer that can be used to pause [sendRequest]. - /// - /// Tests can await this completer to control the execution of `sendRequest`. - Completer? sendRequestCompleter; - - /// The number of times [sendRequest] has been called. - int sendRequestCallCount = 0; - - /// The last message passed to [sendRequest]. - ChatMessage? lastMessage; - - /// The last history passed to [sendRequest]. - Iterable? lastHistory; - - /// The last client capabilities passed to [sendRequest]. - A2UiClientCapabilities? lastClientCapabilities; - - @override - Stream get a2uiMessageStream => _a2uiMessageController.stream; - - @override - Stream get textResponseStream => _textResponseController.stream; - - @override - Stream get errorStream => _errorController.stream; - - @override - ValueListenable get isProcessing => _isProcessing; - - @override - void dispose() { - _a2uiMessageController.close(); - _textResponseController.close(); - _errorController.close(); - _isProcessing.dispose(); - } - - @override - Future sendRequest( - ChatMessage message, { - Iterable? history, - A2UiClientCapabilities? clientCapabilities, - }) async { - _isProcessing.value = true; - try { - sendRequestCallCount++; - lastMessage = message; - lastHistory = history; - lastClientCapabilities = clientCapabilities; - if (sendRequestCompleter != null) { - await sendRequestCompleter!.future; - } - } finally { - _isProcessing.value = false; - } - } - - /// Adds an A2UI message to the stream. - void addA2uiMessage(A2uiMessage message) { - _a2uiMessageController.add(message); - } - - /// Adds a text response to the stream. - void addTextResponse(String response) { - _textResponseController.add(response); - } -} diff --git a/packages/genui/lib/test/validation.dart b/packages/genui/lib/test/validation.dart index c27be5e3a..1b405b539 100644 --- a/packages/genui/lib/test/validation.dart +++ b/packages/genui/lib/test/validation.dart @@ -48,7 +48,7 @@ Future> validateCatalogItemExamples( CatalogItem item, Catalog catalog, ) async { - final Schema schema = A2uiSchemas.surfaceUpdateSchema(catalog); + final Schema schema = A2uiSchemas.updateComponentsSchema(catalog); final errors = []; for (var i = 0; i < item.exampleData.length; i++) { @@ -76,7 +76,7 @@ Future> validateCatalogItemExamples( ); } - final surfaceUpdate = SurfaceUpdate( + final surfaceUpdate = UpdateComponents( surfaceId: 'test-surface', components: components, ); diff --git a/packages/genui/pubspec.yaml b/packages/genui/pubspec.yaml index 84d3983b5..deec7d682 100644 --- a/packages/genui/pubspec.yaml +++ b/packages/genui/pubspec.yaml @@ -21,11 +21,15 @@ dependencies: sdk: flutter flutter_markdown_plus: ^1.0.5 genai_primitives: ^0.2.0 + intl: ^0.20.2 json_schema_builder: ^0.1.3 logging: ^1.3.0 + url_launcher: ^6.3.2 uuid: ^4.4.0 dev_dependencies: + async: ^2.13.0 flutter_test: sdk: flutter network_image_mock: ^2.1.1 + test: ^1.26.2 diff --git a/packages/genui/submodules/a2ui b/packages/genui/submodules/a2ui new file mode 160000 index 000000000..c1397f809 --- /dev/null +++ b/packages/genui/submodules/a2ui @@ -0,0 +1 @@ +Subproject commit c1397f80978b35b728d1aec5b39cdfc2ba1384c4 diff --git a/packages/genui/test/ai_client/tools_test.dart b/packages/genui/test/ai_client/tools_test.dart deleted file mode 100644 index 27d4674c4..000000000 --- a/packages/genui/test/ai_client/tools_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/src/model/tools.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -void main() { - group('AiTool', () { - test('fullName returns correct name', () { - final tool = DynamicAiTool>( - name: 'testTool', - description: 'A test tool.', - invokeFunction: (args) async => {}, - ); - expect(tool.fullName, 'testTool'); - - final toolWithPrefix = DynamicAiTool>( - name: 'testTool', - prefix: 'prefix', - description: 'A test tool.', - invokeFunction: (args) async => {}, - ); - expect(toolWithPrefix.fullName, 'prefix.testTool'); - }); - }); - - group('DynamicAiTool', () { - test('invoke calls invokeFunction', () async { - var called = false; - final tool = DynamicAiTool>( - name: 'testTool', - description: 'A test tool.', - parameters: S.object(properties: {}), - invokeFunction: (args) async { - called = true; - return {}; - }, - ); - - await tool.invoke({}); - expect(called, isTrue); - }); - }); -} diff --git a/packages/genui/test/catalog/core_widgets/audio_player_test.dart b/packages/genui/test/catalog/core_widgets/audio_player_test.dart new file mode 100644 index 000000000..fd675a34f --- /dev/null +++ b/packages/genui/test/catalog/core_widgets/audio_player_test.dart @@ -0,0 +1,58 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +void main() { + testWidgets('AudioPlayer widget renders and has description in semantics', ( + WidgetTester tester, + ) async { + final manager = SurfaceController( + catalogs: [ + Catalog([BasicCatalogItems.audioPlayer], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'AudioPlayer', + properties: { + 'url': 'https://example.com/audio.mp3', + 'description': 'Audio Description', + }, + ), + ]; + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + + expect(find.byType(Placeholder), findsOneWidget); + + // Check for Semantics widget properties directly if find.bySemanticsLabel + // fails + final Semantics semantics = tester.widget( + find + .ancestor( + of: find.byType(Placeholder), + matching: find.byType(Semantics), + ) + .first, + ); + expect(semantics.properties.label, 'Audio Description'); + }); +} diff --git a/packages/genui/test/catalog/core_widgets/button_test.dart b/packages/genui/test/catalog/core_widgets/button_test.dart index 2aabd076f..176062a9e 100644 --- a/packages/genui/test/catalog/core_widgets/button_test.dart +++ b/packages/genui/test/catalog/core_widgets/button_test.dart @@ -11,11 +11,11 @@ void main() { WidgetTester tester, ) async { ChatMessage? message; - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.button, - CoreCatalogItems.text, + BasicCatalogItems.button, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); @@ -23,38 +23,32 @@ void main() { const surfaceId = 'testSurface'; final components = [ const Component( - id: 'button', - componentProperties: { - 'Button': { - 'child': 'button_text', - 'action': {'name': 'testAction'}, + id: 'root', + type: 'Button', + properties: { + 'child': 'button_text', + 'action': { + 'event': {'name': 'testAction'}, }, }, ), const Component( id: 'button_text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Click Me'}, - }, - }, + type: 'Text', + properties: {'text': 'Click Me'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'button', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/card_test.dart b/packages/genui/test/catalog/core_widgets/card_test.dart index 1647aafb5..3d2b2ae16 100644 --- a/packages/genui/test/catalog/core_widgets/card_test.dart +++ b/packages/genui/test/catalog/core_widgets/card_test.dart @@ -8,46 +8,34 @@ import 'package:genui/genui.dart'; void main() { testWidgets('Card widget renders child', (WidgetTester tester) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.card, - CoreCatalogItems.text, + BasicCatalogItems.card, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ - const Component( - id: 'card', - componentProperties: { - 'Card': {'child': 'text'}, - }, - ), + const Component(id: 'root', type: 'Card', properties: {'child': 'text'}), const Component( id: 'text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'This is a card.'}, - }, - }, + type: 'Text', + properties: {'text': 'This is a card.'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'card', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/check_box_test.dart b/packages/genui/test/catalog/core_widgets/check_box_test.dart index 31155fc0e..9d6af0d54 100644 --- a/packages/genui/test/catalog/core_widgets/check_box_test.dart +++ b/packages/genui/test/catalog/core_widgets/check_box_test.dart @@ -10,39 +10,34 @@ void main() { testWidgets('CheckBox widget renders and handles changes', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ - Catalog([CoreCatalogItems.checkBox], catalogId: 'test_catalog'), + Catalog([BasicCatalogItems.checkBox], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'checkbox', - componentProperties: { - 'CheckBox': { - 'label': {'literalString': 'Check me'}, - 'value': {'path': '/myValue'}, - }, + id: 'root', + type: 'CheckBox', + properties: { + 'label': 'Check me', + 'value': {'path': '/myValue'}, }, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'checkbox', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); - manager.dataModelForSurface(surfaceId).update(DataPath('/myValue'), true); + manager.contextFor(surfaceId).dataModel.update(DataPath('/myValue'), true); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -56,7 +51,8 @@ void main() { await tester.tap(find.byType(CheckboxListTile)); expect( manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .getValue(DataPath('/myValue')), isFalse, ); diff --git a/packages/genui/test/catalog/core_widgets/choice_picker_test.dart b/packages/genui/test/catalog/core_widgets/choice_picker_test.dart new file mode 100644 index 000000000..6e878b0de --- /dev/null +++ b/packages/genui/test/catalog/core_widgets/choice_picker_test.dart @@ -0,0 +1,303 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:genui/src/catalog/basic_catalog_widgets/choice_picker.dart'; + +void main() { + // Test case based on jobApplication.1.sample + testWidgets( + 'ChoicePicker mutuallyExclusive handles single String value in DataModel', + (WidgetTester tester) async { + final catalog = Catalog([choicePicker], catalogId: 'test'); + + // Create a controller with a catalog that has ChoicePicker + final controller = SurfaceController(catalogs: [catalog]); + + // Initial message to create surface and components + final createSurface = const CreateSurface( + surfaceId: 'test', + catalogId: 'test', + ); + + final updateData = const UpdateDataModel( + surfaceId: 'test', + value: { + 'experience': '2-5', // Single string value, not a list + }, + path: DataPath.root, + ); + + final updateComponents = const UpdateComponents( + surfaceId: 'test', + components: [ + Component( + id: 'root', + type: 'ChoicePicker', + properties: { + 'label': 'Years of Experience', + 'variant': 'mutuallyExclusive', + 'options': [ + {'label': '0-1', 'value': '0-1'}, + {'label': '2-5', 'value': '2-5'}, + {'label': '5+', 'value': '5+'}, + ], + 'value': {'path': '/experience'}, + }, + ), + ], + ); + + controller.handleMessage(createSurface); + controller.handleMessage(updateData); + controller.handleMessage(updateComponents); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('test')), + ), + ), + ); + + await tester.pumpAndSettle(); + + // Verify it rendered + expect(find.text('Years of Experience'), findsOneWidget); + expect(find.text('0-1'), findsOneWidget); + expect(find.text('2-5'), findsOneWidget); + + // Verify selection (RadioListTile) + // We check if the radio button corresponding to '2-5' is selected. + // In Flutter RadioListTile, we can find the Radio widget. + + final Iterable> radios = tester.widgetList>( + find.byType(Radio), + ); + expect(radios.length, 3); + + // The second radio should be selected + final Radio selectedRadio = radios.elementAt(1); + expect(selectedRadio.value, '2-5'); + expect( + // ignore: deprecated_member_use + selectedRadio.groupValue, + '2-5', + reason: 'Group value should match the selected choice', + ); + + // Update data model to another single string + controller.handleMessage( + UpdateDataModel( + surfaceId: 'test', + path: DataPath('/experience'), + value: '5+', + ), + ); + await tester.pumpAndSettle(); + + final Iterable> radiosAfter = tester + .widgetList>(find.byType(Radio)); + // ignore: deprecated_member_use + expect(radiosAfter.elementAt(2).groupValue, '5+'); + }, + ); + + testWidgets('ChoicePicker multipleSelection handles List value', ( + WidgetTester tester, + ) async { + final catalog = Catalog([choicePicker], catalogId: 'std'); + + final controller = SurfaceController(catalogs: [catalog]); + + final createSurface = const CreateSurface( + surfaceId: 'test2', + catalogId: 'std', + ); + final updateData = const UpdateDataModel( + surfaceId: 'test2', + value: { + 'selections': ['A', 'B'], + }, + path: DataPath.root, + ); + final updateComponents = const UpdateComponents( + surfaceId: 'test2', + components: [ + Component( + id: 'root', + type: 'ChoicePicker', + properties: { + 'label': 'Multi', + 'variant': 'multipleSelection', + 'options': [ + {'label': 'A', 'value': 'A'}, + {'label': 'B', 'value': 'B'}, + {'label': 'C', 'value': 'C'}, + ], + 'value': {'path': '/selections'}, + }, + ), + ], + ); + + controller.handleMessage(createSurface); + controller.handleMessage(updateData); + controller.handleMessage(updateComponents); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('test2')), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Multi'), findsOneWidget); + + // Verify Checkboxes + final Iterable checkboxes = tester.widgetList( + find.byType(Checkbox), + ); + expect(checkboxes.length, 3); + + expect(checkboxes.elementAt(0).value, true); // A + expect(checkboxes.elementAt(1).value, true); // B + expect(checkboxes.elementAt(2).value, false); // C + }); + + testWidgets('ChoicePicker renders chips and supports filtering', ( + WidgetTester tester, + ) async { + final catalog = Catalog([choicePicker], catalogId: 'std'); + final controller = SurfaceController(catalogs: [catalog]); + + final createSurface = const CreateSurface( + surfaceId: 'chipsTest', + catalogId: 'std', + ); + final updateData = const UpdateDataModel( + surfaceId: 'chipsTest', + value: { + 'tags': ['flutter'], + }, + path: DataPath.root, + ); + final updateComponents = const UpdateComponents( + surfaceId: 'chipsTest', + components: [ + Component( + id: 'root', + type: 'ChoicePicker', + properties: { + 'label': 'Tags', + 'variant': 'multipleSelection', + 'displayStyle': 'chips', + 'filterable': true, + 'options': [ + {'label': 'Flutter', 'value': 'flutter'}, + {'label': 'Dart', 'value': 'dart'}, + {'label': 'GenUI', 'value': 'genui'}, + ], + 'value': {'path': '/tags'}, + }, + ), + ], + ); + + controller.handleMessage(createSurface); + controller.handleMessage(updateData); + controller.handleMessage(updateComponents); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('chipsTest')), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Tags'), findsOneWidget); + // Find FilterChips + expect(find.byType(FilterChip), findsNWidgets(3)); + expect(find.text('Flutter'), findsOneWidget); + + // Verify 'Flutter' is selected + final FilterChip flutterChip = tester.widget( + find.widgetWithText(FilterChip, 'Flutter'), + ); + expect(flutterChip.selected, true); + + // Verify filterable TextField exists + expect(find.byType(TextField), findsOneWidget); + + // Filter by "Gen" + await tester.enterText(find.byType(TextField), 'Gen'); + await tester.pumpAndSettle(); + + // Flutter and Dart should be hidden (or at least filtered out visually) + // The implementation might return SizedBox.shrink for filtered items. + // Let's verify visible widgets. + expect(find.text('GenUI'), findsOneWidget); + expect(find.text('Flutter'), findsNothing); + expect(find.text('Dart'), findsNothing); + }); + + testWidgets( + 'ChoicePicker handles null/missing data in valid path reference', + (WidgetTester tester) async { + final catalog = Catalog([choicePicker], catalogId: 'std'); + final controller = SurfaceController(catalogs: [catalog]); + + final createSurface = const CreateSurface( + surfaceId: 'nullTest', + catalogId: 'std', + ); + // Note: We are NOT sending UpdateDataModel with the value initially. + final updateComponents = const UpdateComponents( + surfaceId: 'nullTest', + components: [ + Component( + id: 'root', + type: 'ChoicePicker', + properties: { + 'label': 'Null Check', + 'variant': 'multipleSelection', + 'options': [ + {'label': 'A', 'value': 'A'}, + ], + // Points to a path that doesn't exist yet + 'value': {'path': '/missing_path'}, + }, + ), + ], + ); + + controller.handleMessage(createSurface); + controller.handleMessage(updateComponents); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: controller.contextFor('nullTest')), + ), + ), + ); + await tester.pumpAndSettle(); + + // Should render without error (title visible) + expect(find.text('Null Check'), findsOneWidget); + // Should have no selections + final Iterable checkboxes = tester.widgetList( + find.byType(Checkbox), + ); + expect(checkboxes.length, 1); + expect(checkboxes.first.value, false); + }, + ); +} diff --git a/packages/genui/test/catalog/core_widgets/column_test.dart b/packages/genui/test/catalog/core_widgets/column_test.dart index 4f2cdd597..432e6f200 100644 --- a/packages/genui/test/catalog/core_widgets/column_test.dart +++ b/packages/genui/test/catalog/core_widgets/column_test.dart @@ -8,58 +8,41 @@ import 'package:genui/genui.dart'; void main() { testWidgets('Column widget renders children', (WidgetTester tester) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.column, - CoreCatalogItems.text, + BasicCatalogItems.column, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'column', - componentProperties: { - 'Column': { - 'children': { - 'explicitList': ['text1', 'text2'], - }, - }, - }, - ), - const Component( - id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, + id: 'root', + type: 'Column', + properties: { + 'children': ['text1', 'text2'], }, ), + const Component(id: 'text1', type: 'Text', properties: {'text': 'First'}), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, - }, + type: 'Text', + properties: {'text': 'Second'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'column', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -71,68 +54,46 @@ void main() { testWidgets('Column widget applies weight property to children', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.column, - CoreCatalogItems.text, + BasicCatalogItems.column, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'column', - componentProperties: { - 'Column': { - 'children': { - 'explicitList': ['text1', 'text2', 'text3'], - }, - }, + id: 'root', + type: 'Column', + properties: { + 'children': ['text1', 'text2', 'text3'], }, ), const Component( id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, - }, - weight: 1, + type: 'Text', + properties: {'text': 'First', 'weight': 1}, ), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, - }, - weight: 2, - ), - const Component( - id: 'text3', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Third'}, - }, - }, + type: 'Text', + properties: {'text': 'Second', 'weight': 2}, ), + const Component(id: 'text3', type: 'Text', properties: {'text': 'Third'}), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'column', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/date_time_input_test.dart b/packages/genui/test/catalog/core_widgets/date_time_input_test.dart index 2cfe0fd08..d7abf0f50 100644 --- a/packages/genui/test/catalog/core_widgets/date_time_input_test.dart +++ b/packages/genui/test/catalog/core_widgets/date_time_input_test.dart @@ -9,13 +9,14 @@ import 'package:genui/genui.dart'; void main() { testWidgets('renders and handles explicit updates', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('datetime', { + final (SurfaceHost manager, String surfaceId) = setup('datetime', { 'value': {'path': '/myDateTime'}, 'enableTime': false, }); manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .update(DataPath('/myDateTime'), '2025-10-15'); await robot.pumpSurface(manager, surfaceId); @@ -28,7 +29,7 @@ void main() { ) async { final robot = DateTimeInputRobot(tester); - var (GenUiHost manager, String surfaceId) = setup('datetime_default', { + var (SurfaceHost manager, String surfaceId) = setup('datetime_default', { 'value': {'path': '/myDateTimeDefault'}, }); await robot.pumpSurface(manager, surfaceId); @@ -52,12 +53,13 @@ void main() { group('combined mode', () { testWidgets('aborts update when time picker is cancelled', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('combined_mode', { + final (SurfaceHost manager, String surfaceId) = setup('combined_mode', { 'value': {'path': '/myDateTime'}, }); manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .update(DataPath('/myDateTime'), '2022-01-01T14:30:00'); await robot.pumpSurface(manager, surfaceId); @@ -69,7 +71,8 @@ void main() { await robot.cancelPicker(); final String? value = manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .getValue(DataPath('/myDateTime')); expect(value, equals('2022-01-01T14:30:00')); }); @@ -78,7 +81,7 @@ void main() { group('time only mode', () { testWidgets('aborts when time picker is cancelled', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('time_only_mode', { + final (SurfaceHost manager, String surfaceId) = setup('time_only_mode', { 'value': {'path': '/myTime'}, 'enableDate': false, }); @@ -90,20 +93,25 @@ void main() { await robot.cancelPicker(); final String? value = manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .getValue(DataPath('/myTime')); expect(value, isNull); }); testWidgets('parses initial value correctly', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('time_only_parsing', { - 'value': {'path': '/myTimeProp'}, - 'enableDate': false, - }); + final (SurfaceHost manager, String surfaceId) = setup( + 'time_only_parsing', + { + 'value': {'path': '/myTimeProp'}, + 'enableDate': false, + }, + ); manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .update(DataPath('/myTimeProp'), '14:32:00'); await robot.pumpSurface(manager, surfaceId); @@ -120,13 +128,14 @@ void main() { testWidgets('updates immediately with date-only string after ' 'date selection', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('date_only_mode', { + final (SurfaceHost manager, String surfaceId) = setup('date_only_mode', { 'value': {'path': '/myDate'}, 'enableTime': false, }); manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .update(DataPath('/myDate'), '2022-01-01'); await robot.pumpSurface(manager, surfaceId); @@ -135,7 +144,8 @@ void main() { await robot.selectDate('20'); final String? value = manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .getValue(DataPath('/myDate')); expect(value, isNotNull); // Verify that no time is included in the value. @@ -149,10 +159,10 @@ void main() { group('date range configuration', () { testWidgets('respects custom firstDate and lastDate', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('custom_range', { + final (SurfaceHost manager, String surfaceId) = setup('custom_range', { 'value': {'path': '/myDate'}, - 'firstDate': '2020-01-01', - 'lastDate': '2030-12-31', + 'min': '2020-01-01', + 'max': '2030-12-31', }); await robot.pumpSurface(manager, surfaceId); @@ -170,7 +180,7 @@ void main() { testWidgets('defaults to -9999 to 9999 when not specified', (tester) async { final robot = DateTimeInputRobot(tester); - final (GenUiHost manager, String surfaceId) = setup('default_range', { + final (SurfaceHost manager, String surfaceId) = setup('default_range', { 'value': {'path': '/myDate'}, }); @@ -186,29 +196,55 @@ void main() { await robot.cancelPicker(); }); }); + + group('validation', () { + testWidgets('shows error when check fails', (tester) async { + final robot = DateTimeInputRobot(tester); + final (SurfaceHost manager, String surfaceId) = setup('validation_test', { + 'value': {'path': '/myDate'}, + 'checks': [ + { + 'condition': { + 'call': 'required', + 'args': { + 'value': {'path': '/myDate'}, + }, + }, + 'message': 'Date is required', + }, + ], + }); + + await robot.pumpSurface(manager, surfaceId); + robot.expectError('Date is required'); + + manager + .contextFor(surfaceId) + .dataModel + .update(DataPath('/myDate'), '2022-01-01'); + await robot.pumpSurface(manager, surfaceId); + robot.expectNoError(); + }); + }); } -(GenUiHost, String) setup(String componentId, Map props) { +(SurfaceHost, String) setup(String componentId, Map props) { final catalog = Catalog([ - CoreCatalogItems.dateTimeInput, + BasicCatalogItems.dateTimeInput, ], catalogId: 'test_catalog'); - final manager = A2uiMessageProcessor(catalogs: [catalog]); + final manager = SurfaceController(catalogs: [catalog]); const surfaceId = 'testSurface'; final components = [ - Component(id: componentId, componentProperties: {'DateTimeInput': props}), + Component(id: 'root', type: 'DateTimeInput', properties: props), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - BeginRendering( - surfaceId: surfaceId, - root: componentId, - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); return (manager, surfaceId); @@ -219,11 +255,11 @@ class DateTimeInputRobot { DateTimeInputRobot(this.tester); - Future pumpSurface(GenUiHost manager, String surfaceId) async { + Future pumpSurface(SurfaceHost manager, String surfaceId) async { await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -231,7 +267,7 @@ class DateTimeInputRobot { } Future openPicker(String componentId) async { - await tester.tap(find.byKey(Key(componentId))); + await tester.tap(find.byKey(const Key('root'))); await tester.pumpAndSettle(); } @@ -247,11 +283,11 @@ class DateTimeInputRobot { } void expectInputText(String componentId, String text) { - final Finder finder = find.byKey(Key('${componentId}_text')); + final Finder finder = find.byKey(const Key('root_text')); expect(finder, findsOneWidget); final String actualText = tester.widget(finder).data!; if (actualText != text) { - print('EXPECTATION FAILED: Expected "$text", found "$actualText"'); + // Expectation will fail below and show the diff. } expect(actualText, text); } @@ -267,4 +303,18 @@ class DateTimeInputRobot { void expectTimePickerHidden() { expect(find.text('Select time'), findsNothing); } + + void expectError(String errorText) { + final Finder finder = find.byType(InputDecorator); + expect(finder, findsOneWidget); + final InputDecorator decorator = tester.widget(finder); + expect(decorator.decoration.errorText, errorText); + } + + void expectNoError() { + final Finder finder = find.byType(InputDecorator); + expect(finder, findsOneWidget); + final InputDecorator decorator = tester.widget(finder); + expect(decorator.decoration.errorText, isNull); + } } diff --git a/packages/genui/test/catalog/core_widgets/divider_test.dart b/packages/genui/test/catalog/core_widgets/divider_test.dart index 811d20095..853dac3ee 100644 --- a/packages/genui/test/catalog/core_widgets/divider_test.dart +++ b/packages/genui/test/catalog/core_widgets/divider_test.dart @@ -8,33 +8,26 @@ import 'package:genui/genui.dart'; void main() { testWidgets('Divider widget renders', (WidgetTester tester) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ - Catalog([CoreCatalogItems.divider], catalogId: 'test_catalog'), + Catalog([BasicCatalogItems.divider], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ - const Component( - id: 'divider', - componentProperties: {'Divider': {}}, - ), + const Component(id: 'root', type: 'Divider', properties: {}), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'divider', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/icon_test.dart b/packages/genui/test/catalog/core_widgets/icon_test.dart index 3e23a406f..8cfa5850c 100644 --- a/packages/genui/test/catalog/core_widgets/icon_test.dart +++ b/packages/genui/test/catalog/core_widgets/icon_test.dart @@ -10,37 +10,26 @@ void main() { testWidgets('Icon widget renders with literal string', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ - Catalog([CoreCatalogItems.icon], catalogId: 'test_catalog'), + Catalog([BasicCatalogItems.icon], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ - const Component( - id: 'icon', - componentProperties: { - 'Icon': { - 'name': {'literalString': 'add'}, - }, - }, - ), + const Component(id: 'root', type: 'Icon', properties: {'name': 'add'}), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'icon', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -51,44 +40,39 @@ void main() { testWidgets('Icon widget renders with data binding', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ - Catalog([CoreCatalogItems.icon], catalogId: 'test_catalog'), + Catalog([BasicCatalogItems.icon], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'icon', - componentProperties: { - 'Icon': { - 'name': {'path': '/iconName'}, - }, + id: 'root', + type: 'Icon', + properties: { + 'name': {'path': '/iconName'}, }, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const DataModelUpdate( + UpdateDataModel( surfaceId: 'testSurface', - path: '/iconName', - contents: 'close', + path: DataPath('/iconName'), + value: 'close', ), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'icon', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/image_test.dart b/packages/genui/test/catalog/core_widgets/image_test.dart new file mode 100644 index 000000000..4fa8af024 --- /dev/null +++ b/packages/genui/test/catalog/core_widgets/image_test.dart @@ -0,0 +1,213 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +class _FakeHttpClient extends Fake implements HttpClient { + @override + Future getUrl(Uri url) async { + throw const SocketException('Failed to connect'); + } +} + +void main() { + testWidgets('Image widget handles network error gracefully', ( + WidgetTester tester, + ) async { + await HttpOverrides.runZoned(() async { + final manager = SurfaceController( + catalogs: [ + Catalog([BasicCatalogItems.image], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'Image', + properties: {'url': 'https://example.com/nonexistent.png'}, + ), + ]; + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + + // Pump to allow image loading to fail + await tester.pump(); + await tester.pump(); + + // We expect the check that the image failed and is showing the broken + // image icon. + expect(find.byType(Image), findsOneWidget); + expect(find.byIcon(Icons.broken_image), findsOneWidget); + }, createHttpClient: (context) => _FakeHttpClient()); + }); + + testWidgets('Image widget loads successfully from network', ( + WidgetTester tester, + ) async { + await HttpOverrides.runZoned(() async { + final manager = SurfaceController( + catalogs: [ + Catalog([BasicCatalogItems.image], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'Image', + properties: {'url': 'https://example.com/image.png'}, + ), + ]; + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + + // Verify Image widget is present + expect(find.byType(Image), findsOneWidget); + // We can't easily verify the pixels without comprehensive mocking of + // HttpClientResponse but we can verify no error icon. + expect(find.byIcon(Icons.broken_image), findsNothing); + }, createHttpClient: (context) => _FakeSuccessHttpClient()); + }); +} + +class _FakeSuccessHttpClient extends Fake implements HttpClient { + @override + Future getUrl(Uri url) async { + return _FakeHttpClientRequest(); + } +} + +class _FakeHttpClientRequest extends Fake implements HttpClientRequest { + @override + Future close() async { + return _FakeHttpClientResponse(); + } +} + +class _FakeHttpClientResponse extends Fake implements HttpClientResponse { + @override + int get statusCode => 200; + + @override + int get contentLength => kTransparentImage.length; + + @override + HttpClientResponseCompressionState get compressionState => + HttpClientResponseCompressionState.notCompressed; + + @override + StreamSubscription> listen( + void Function(List event)? onData, { + Function? onError, + void Function()? onDone, + bool? cancelOnError, + }) { + return Stream>.fromIterable([kTransparentImage]).listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + +final List kTransparentImage = [ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, + 0x42, + 0x60, + 0x82, +]; diff --git a/packages/genui/test/catalog/core_widgets/list_test.dart b/packages/genui/test/catalog/core_widgets/list_test.dart index 8d2b6e922..ecca3f189 100644 --- a/packages/genui/test/catalog/core_widgets/list_test.dart +++ b/packages/genui/test/catalog/core_widgets/list_test.dart @@ -8,63 +8,95 @@ import 'package:genui/genui.dart'; void main() { testWidgets('List widget renders children', (WidgetTester tester) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.list, - CoreCatalogItems.text, + BasicCatalogItems.list, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'list', - componentProperties: { - 'List': { - 'children': { - 'explicitList': ['text1', 'text2'], - }, - }, + id: 'root', + type: 'List', + properties: { + 'children': ['text1', 'text2'], }, ), + const Component(id: 'text1', type: 'Text', properties: {'text': 'First'}), const Component( - id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, - }, + id: 'text2', + type: 'Text', + properties: {'text': 'Second'}, ), + ]; + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + + expect(find.text('Second'), findsOneWidget); + }); + + testWidgets('List widget respects align property', ( + WidgetTester tester, + ) async { + final manager = SurfaceController( + catalogs: [ + Catalog([ + BasicCatalogItems.list, + BasicCatalogItems.text, + ], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + final components = [ const Component( - id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, + id: 'root', + type: 'List', + properties: { + 'align': 'center', + 'children': ['text1'], }, ), + const Component( + id: 'text1', + type: 'Text', + properties: {'text': 'Center'}, + ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'list', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); - expect(find.text('First'), findsOneWidget); - expect(find.text('Second'), findsOneWidget); + expect(find.text('Center'), findsOneWidget); + // Verify alignment logic by finding the Flex widget wrapping the child. + final Flex flexWidget = tester.widget( + find.ancestor(of: find.text('Center'), matching: find.byType(Flex)).first, + ); + expect(flexWidget.crossAxisAlignment, CrossAxisAlignment.center); }); } diff --git a/packages/genui/test/catalog/core_widgets/modal_test.dart b/packages/genui/test/catalog/core_widgets/modal_test.dart index 1a70c6675..f5d7e7563 100644 --- a/packages/genui/test/catalog/core_widgets/modal_test.dart +++ b/packages/genui/test/catalog/core_widgets/modal_test.dart @@ -10,72 +10,57 @@ void main() { testWidgets('Modal widget renders and handles taps', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.modal, - CoreCatalogItems.button, - CoreCatalogItems.text, + BasicCatalogItems.modal, + BasicCatalogItems.button, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'modal', - componentProperties: { - 'Modal': {'entryPointChild': 'button', 'contentChild': 'text'}, - }, + id: 'root', + type: 'Modal', + properties: {'trigger': 'button', 'content': 'text'}, ), const Component( id: 'button', - componentProperties: { - 'Button': { - 'child': 'button_text', - 'action': { + type: 'Button', + properties: { + 'child': 'button_text', + 'action': { + 'event': { 'name': 'showModal', - 'context': [ - { - 'key': 'modalId', - 'value': {'literalString': 'modal'}, - }, - ], + 'context': {'modalId': 'root'}, }, }, }, ), const Component( id: 'button_text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Open Modal'}, - }, - }, + type: 'Text', + properties: {'text': 'Open Modal'}, ), const Component( id: 'text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'This is a modal.'}, - }, - }, + type: 'Text', + properties: {'text': 'This is a modal.'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'modal', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); diff --git a/packages/genui/test/catalog/core_widgets/multiple_choice_test.dart b/packages/genui/test/catalog/core_widgets/multiple_choice_test.dart deleted file mode 100644 index eae2245af..000000000 --- a/packages/genui/test/catalog/core_widgets/multiple_choice_test.dart +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; - -void main() { - testWidgets('MultipleChoice widget renders and handles changes', ( - WidgetTester tester, - ) async { - final processor = A2uiMessageProcessor( - catalogs: [ - Catalog([ - CoreCatalogItems.multipleChoice, - CoreCatalogItems.text, - ], catalogId: 'test_catalog'), - ], - ); - const surfaceId = 'testSurface'; - final components = [ - const Component( - id: 'multiple_choice', - componentProperties: { - 'MultipleChoice': { - 'selections': {'path': '/mySelections'}, - 'options': [ - { - 'label': {'literalString': 'Option 1'}, - 'value': '1', - }, - { - 'label': {'literalString': 'Option 2'}, - 'value': '2', - }, - ], - }, - }, - ), - ]; - processor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), - ); - processor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'multiple_choice', - catalogId: 'test_catalog', - ), - ); - processor.dataModelForSurface(surfaceId).update(DataPath('/mySelections'), [ - '1', - ]); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: GenUiSurface(host: processor, surfaceId: surfaceId), - ), - ), - ); - - expect(find.text('Option 1'), findsOneWidget); - expect(find.text('Option 2'), findsOneWidget); - final CheckboxListTile checkbox1 = tester.widget( - find.byType(CheckboxListTile).first, - ); - expect(checkbox1.value, isTrue); - final CheckboxListTile checkbox2 = tester.widget( - find.byType(CheckboxListTile).last, - ); - expect(checkbox2.value, isFalse); - - await tester.tap(find.text('Option 2')); - expect( - processor - .dataModelForSurface(surfaceId) - .getValue>(DataPath('/mySelections')), - ['1', '2'], - ); - }); - - testWidgets( - 'MultipleChoice widget handles non-integer maxAllowedSelections from JSON', - (WidgetTester tester) async { - final processor = A2uiMessageProcessor( - catalogs: [ - Catalog([ - CoreCatalogItems.multipleChoice, - CoreCatalogItems.text, - ], catalogId: 'test_catalog'), - ], - ); - const surfaceId = 'testSurface'; - - final components = [ - const Component( - id: 'multiple_choice', - componentProperties: { - 'MultipleChoice': { - 'selections': {'path': '/mySelections'}, - 'maxAllowedSelections': 3.0, - 'options': [ - { - 'label': {'literalString': 'Option 1'}, - 'value': '1', - }, - { - 'label': {'literalString': 'Option 2'}, - 'value': '2', - }, - ], - }, - }, - ), - ]; - - processor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), - ); - processor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'multiple_choice', - catalogId: 'test_catalog', - ), - ); - - processor - .dataModelForSurface(surfaceId) - .update(DataPath('/mySelections'), []); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: GenUiSurface(host: processor, surfaceId: surfaceId), - ), - ), - ); - - // No exception was thrown. - }, - ); -} diff --git a/packages/genui/test/catalog/core_widgets/row_test.dart b/packages/genui/test/catalog/core_widgets/row_test.dart index c43f28908..c667d49ce 100644 --- a/packages/genui/test/catalog/core_widgets/row_test.dart +++ b/packages/genui/test/catalog/core_widgets/row_test.dart @@ -8,58 +8,41 @@ import 'package:genui/genui.dart'; void main() { testWidgets('Row widget renders children', (WidgetTester tester) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.row, - CoreCatalogItems.text, + BasicCatalogItems.row, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'row', - componentProperties: { - 'Row': { - 'children': { - 'explicitList': ['text1', 'text2'], - }, - }, - }, - ), - const Component( - id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, + id: 'root', + type: 'Row', + properties: { + 'children': ['text1', 'text2'], }, ), + const Component(id: 'text1', type: 'Text', properties: {'text': 'First'}), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, - }, + type: 'Text', + properties: {'text': 'Second'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'row', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -71,68 +54,46 @@ void main() { testWidgets('Row widget applies weight property to children', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.row, - CoreCatalogItems.text, + BasicCatalogItems.row, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'row', - componentProperties: { - 'Row': { - 'children': { - 'explicitList': ['text1', 'text2', 'text3'], - }, - }, + id: 'root', + type: 'Row', + properties: { + 'children': ['text1', 'text2', 'text3'], }, ), const Component( id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, - }, - weight: 1, + type: 'Text', + properties: {'text': 'First', 'weight': 1}, ), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, - }, - weight: 2, - ), - const Component( - id: 'text3', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Third'}, - }, - }, + type: 'Text', + properties: {'text': 'Second', 'weight': 2}, ), + const Component(id: 'text3', type: 'Text', properties: {'text': 'Third'}), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'row', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -151,17 +112,74 @@ void main() { expect(flexibleWidgets[1].flex, 2); // Check that the correct children are wrapped - expect( - find.ancestor(of: find.text('First'), matching: find.byType(Flexible)), - findsOneWidget, - ); - expect( - find.ancestor(of: find.text('Second'), matching: find.byType(Flexible)), - findsOneWidget, - ); expect( find.ancestor(of: find.text('Third'), matching: find.byType(Flexible)), findsNothing, ); }); + + testWidgets('Row widget renders dynamic children from template', ( + WidgetTester tester, + ) async { + final manager = SurfaceController( + catalogs: [ + Catalog([ + BasicCatalogItems.row, + BasicCatalogItems.text, + ], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + + // Initial data with items + manager.handleMessage( + const UpdateDataModel( + surfaceId: surfaceId, + value: { + 'items': ['Item 1', 'Item 2', 'Item 3'], + }, + ), + ); + + final components = [ + const Component( + id: 'root', + type: 'Row', + properties: { + 'children': {'path': '/items', 'componentId': 'textItem'}, + }, + ), + const Component( + id: 'textItem', + type: 'Text', + properties: { + 'text': {'path': ''}, // Relative path (empty = self) + }, + ), + ]; + + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + // CreateSurface must be sent to initialize the surface, can be before or after components/data updates are processed + // if buffering is working, but usually it comes first in stream. + // In this test setup, `manager.handleMessage` processes synchronously? + // If CreateSurface is last, it might trigger the build. + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Item 1'), findsOneWidget); + expect(find.text('Item 2'), findsOneWidget); + expect(find.text('Item 3'), findsOneWidget); + }); } diff --git a/packages/genui/test/catalog/core_widgets/slider_test.dart b/packages/genui/test/catalog/core_widgets/slider_test.dart index 43de16ebc..85cdfa8cd 100644 --- a/packages/genui/test/catalog/core_widgets/slider_test.dart +++ b/packages/genui/test/catalog/core_widgets/slider_test.dart @@ -10,38 +10,33 @@ void main() { testWidgets('Slider widget renders and handles changes', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ - Catalog([CoreCatalogItems.slider], catalogId: 'test_catalog'), + Catalog([BasicCatalogItems.slider], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'slider', - componentProperties: { - 'Slider': { - 'value': {'path': '/myValue'}, - }, + id: 'root', + type: 'Slider', + properties: { + 'value': {'path': '/myValue'}, }, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'slider', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); - manager.dataModelForSurface(surfaceId).update(DataPath('/myValue'), 0.5); + manager.contextFor(surfaceId).dataModel.update(DataPath('/myValue'), 0.5); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -52,9 +47,46 @@ void main() { await tester.drag(find.byType(Slider), const Offset(100, 0)); expect( manager - .dataModelForSurface(surfaceId) + .contextFor(surfaceId) + .dataModel .getValue(DataPath('/myValue')), greaterThan(0.5), ); }); + + testWidgets('Slider widget renders label', (WidgetTester tester) async { + final manager = SurfaceController( + catalogs: [ + Catalog([BasicCatalogItems.slider], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + final components = [ + const Component( + id: 'root', + type: 'Slider', + properties: { + 'value': {'path': '/myValue'}, + 'label': 'Volume', + }, + ), + ]; + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + manager.contextFor(surfaceId).dataModel.update(DataPath('/myValue'), 0.5); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + + expect(find.text('Volume'), findsOneWidget); + }); } diff --git a/packages/genui/test/catalog/core_widgets/tabs_test.dart b/packages/genui/test/catalog/core_widgets/tabs_test.dart index 11acefdde..af18f2867 100644 --- a/packages/genui/test/catalog/core_widgets/tabs_test.dart +++ b/packages/genui/test/catalog/core_widgets/tabs_test.dart @@ -10,65 +10,49 @@ void main() { testWidgets('Tabs widget renders and handles taps', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( + final manager = SurfaceController( catalogs: [ Catalog([ - CoreCatalogItems.tabs, - CoreCatalogItems.text, + BasicCatalogItems.tabs, + BasicCatalogItems.text, ], catalogId: 'test_catalog'), ], ); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'tabs', - componentProperties: { - 'Tabs': { - 'tabItems': [ - { - 'title': {'literalString': 'Tab 1'}, - 'child': 'text1', - }, - { - 'title': {'literalString': 'Tab 2'}, - 'child': 'text2', - }, - ], - }, + id: 'root', + type: 'Tabs', + properties: { + 'component': 'Tabs', + 'tabs': [ + {'label': 'Tab 1', 'content': 'text1'}, + {'label': 'Tab 2', 'content': 'text2'}, + ], }, ), const Component( id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'This is the first tab.'}, - }, - }, + type: 'Text', + properties: {'component': 'Text', 'text': 'This is the first tab.'}, ), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'This is the second tab.'}, - }, - }, + type: 'Text', + properties: {'component': 'Text', 'text': 'This is the second tab.'}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'tabs', - catalogId: 'test_catalog', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -84,4 +68,93 @@ void main() { expect(find.text('This is the first tab.'), findsNothing); expect(find.text('This is the second tab.'), findsOneWidget); }); + + testWidgets('Tabs activeTab binding works', (WidgetTester tester) async { + final manager = SurfaceController( + catalogs: [ + Catalog([ + BasicCatalogItems.tabs, + BasicCatalogItems.text, + ], catalogId: 'test_catalog'), + ], + ); + const surfaceId = 'testSurface'; + + // Initialize data model with tab 1 (index 1) active + manager.handleMessage( + UpdateDataModel( + surfaceId: surfaceId, + path: DataPath('/'), + value: {'currentTab': 1}, + ), + ); + + final components = [ + const Component( + id: 'root', + type: 'Tabs', + properties: { + 'component': 'Tabs', + 'activeTab': {'path': 'currentTab'}, + 'tabs': [ + {'label': 'Tab 1', 'content': 'text1'}, + {'label': 'Tab 2', 'content': 'text2'}, + ], + }, + ), + const Component( + id: 'text1', + type: 'Text', + properties: {'component': 'Text', 'text': 'Content 1'}, + ), + const Component( + id: 'text2', + type: 'Text', + properties: {'component': 'Text', 'text': 'Content 2'}, + ), + ]; + + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + // Initial build + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + await tester.pumpAndSettle(); + + // Verify Tab 2 is active (index 1) + expect(find.text('Content 2'), findsOneWidget); + expect(find.text('Content 1'), findsNothing); + + // Update data model to switch to Tab 1 (index 0) + manager.handleMessage( + UpdateDataModel( + surfaceId: 'testSurface', + path: DataPath('/currentTab'), + value: 0, + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('Content 1'), findsOneWidget); + expect(find.text('Content 2'), findsNothing); + + // Tap Tab 2 + await tester.tap(find.text('Tab 2')); + await tester.pumpAndSettle(); + expect(find.text('Content 2'), findsOneWidget); + + // Verify data model updated + final DataModel dataModel = manager.contextFor(surfaceId).dataModel; + expect(dataModel.getValue(DataPath('currentTab')), 1); + }); } diff --git a/packages/genui/test/catalog/core_widgets/text_field_test.dart b/packages/genui/test/catalog/core_widgets/text_field_test.dart index 827c088bb..c8fc538b8 100644 --- a/packages/genui/test/catalog/core_widgets/text_field_test.dart +++ b/packages/genui/test/catalog/core_widgets/text_field_test.dart @@ -9,47 +9,38 @@ import 'package:genui/genui.dart'; void main() { testWidgets('TextField with no weight in Row defaults to weight: 1 ' 'and expands', (WidgetTester tester) async { - final a2uiProcessor = A2uiMessageProcessor( - catalogs: [CoreCatalogItems.asCatalog()], + final a2uiProcessor = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], ); + addTearDown(a2uiProcessor.dispose); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'row', - componentProperties: { - 'Row': { - 'children': { - 'explicitList': ['text_field'], - }, - }, + id: 'root', + type: 'Row', + properties: { + 'children': ['text_field'], }, ), const Component( id: 'text_field', - componentProperties: { - 'TextField': { - 'label': {'literalString': 'Input'}, - }, - }, + type: 'TextField', + properties: {'label': 'Input'}, // "weight" property is left unset. ), ]; a2uiProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); a2uiProcessor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'row', - catalogId: 'a2ui.org:standard_catalog_0_8_0', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: a2uiProcessor, surfaceId: surfaceId), + body: Surface(surfaceContext: a2uiProcessor.contextFor(surfaceId)), ), ), ); @@ -72,47 +63,37 @@ void main() { testWidgets('TextField in Row (with weight) expands', ( WidgetTester tester, ) async { - final manager = A2uiMessageProcessor( - catalogs: [CoreCatalogItems.asCatalog()], + final manager = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], ); + addTearDown(manager.dispose); const surfaceId = 'testSurface'; final components = [ const Component( - id: 'row', - componentProperties: { - 'Row': { - 'children': { - 'explicitList': ['text_field'], - }, - }, + id: 'root', + type: 'Row', + properties: { + 'children': ['text_field'], }, ), const Component( id: 'text_field', - componentProperties: { - 'TextField': { - 'label': {'literalString': 'Input'}, - }, - }, - weight: 1, + type: 'TextField', + properties: {'label': 'Input', 'weight': 1}, ), ]; manager.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + UpdateComponents(surfaceId: surfaceId, components: components), ); manager.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'row', - catalogId: 'a2ui.org:standard_catalog_0_8_0', - ), + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: manager, surfaceId: surfaceId), + body: Surface(surfaceContext: manager.contextFor(surfaceId)), ), ), ); @@ -131,4 +112,130 @@ void main() { final Size size = tester.getSize(find.byType(TextField)); expect(size.width, 800.0); }); + + testWidgets('TextField validation checks work', (WidgetTester tester) async { + final manager = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], + ); + addTearDown(manager.dispose); + const surfaceId = 'validationTest'; + // Initialize with invalid value + manager.handleMessage( + UpdateDataModel( + surfaceId: surfaceId, + path: DataPath('/myValue'), + value: 'initial', + ), + ); + + final components = [ + const Component( + id: 'root', + type: 'TextField', + properties: { + 'label': 'Input', + 'value': {'path': 'inputValue'}, + 'checks': [ + { + 'message': 'Must be at least 6 chars', + 'condition': { + 'call': 'length', + 'args': { + 'value': {'path': 'inputValue'}, + 'min': 6, + }, + }, + }, + ], + }, + ), + ]; + + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + await tester.pumpAndSettle(); + + // Verify error text is shown + expect(find.text('Must be at least 6 chars'), findsOneWidget); + + // Update with valid value + await tester.enterText(find.byType(TextField), 'valid value'); + await tester.pumpAndSettle(); + + expect(find.text('Must be at least 6 chars'), findsNothing); + }); + testWidgets('TextField validation using condition wrapper and call key', ( + WidgetTester tester, + ) async { + final manager = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], + ); + addTearDown(manager.dispose); + const surfaceId = 'validationWrapperTest'; + // Initialize with invalid value (empty string) + manager.handleMessage( + UpdateDataModel(surfaceId: surfaceId, path: DataPath('/name'), value: ''), + ); + + final components = [ + const Component( + id: 'root', + type: 'TextField', + properties: { + 'label': 'Name', + 'value': {'path': '/name'}, + 'checks': [ + { + // Using "condition" wrapper and "call" instead of "func" + // Args as list, as expected by function registry + 'condition': { + 'call': 'required', + 'args': { + 'value': {'path': '/name'}, + }, + }, + 'message': 'Name required', + }, + ], + }, + ), + ]; + + manager.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + manager.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: basicCatalogId), + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Surface(surfaceContext: manager.contextFor(surfaceId)), + ), + ), + ); + await tester.pumpAndSettle(); + + // Empty value should trigger required + expect(find.text('Name required'), findsOneWidget); + + // Update with valid value + await tester.enterText(find.byType(TextField), 'Alice'); + await tester.pumpAndSettle(); + + expect(find.text('Name required'), findsNothing); + }); } diff --git a/packages/genui/test/catalog/core_widgets/text_test.dart b/packages/genui/test/catalog/core_widgets/text_test.dart index 7ea0a8ba8..bcca1e64e 100644 --- a/packages/genui/test/catalog/core_widgets/text_test.dart +++ b/packages/genui/test/catalog/core_widgets/text_test.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/src/catalog/core_widgets/text.dart'; +import 'package:genui/src/catalog/basic_catalog_widgets/text.dart'; import 'package:genui/src/model/catalog_item.dart'; import 'package:genui/src/model/data_model.dart'; import 'package:genui/src/model/ui_models.dart'; @@ -20,15 +20,15 @@ void main() { builder: (context) => Scaffold( body: text.widgetBuilder( CatalogItemContext( - data: { - 'text': {'literalString': 'Hello World'}, - }, + data: {'text': 'Hello World'}, id: 'test_text', + type: 'Text', buildChild: (_, [_]) => const SizedBox(), dispatchEvent: (UiEvent event) {}, buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -49,16 +49,15 @@ void main() { builder: (context) => Scaffold( body: text.widgetBuilder( CatalogItemContext( - data: { - 'text': {'literalString': 'Heading 1'}, - 'usageHint': 'h1', - }, + data: {'text': 'Heading 1', 'variant': 'h1'}, id: 'test_text_h1', + type: 'Text', buildChild: (_, [_]) => const SizedBox(), dispatchEvent: (UiEvent event) {}, buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -103,15 +102,15 @@ void main() { builder: (context) => Scaffold( body: text.widgetBuilder( CatalogItemContext( - data: { - 'text': {'literalString': 'Hello **Bold**'}, - }, + data: {'text': 'Hello **Bold**'}, id: 'test_text_markdown', + type: 'Text', buildChild: (_, [_]) => const SizedBox(), dispatchEvent: (UiEvent event) {}, buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -145,15 +144,15 @@ void main() { style: const TextStyle(color: requiredColor), child: text.widgetBuilder( CatalogItemContext( - data: { - 'text': {'literalString': 'Contrast Text'}, - }, + data: {'text': 'Contrast Text'}, id: 'test_contrast', + type: 'Text', buildChild: (_, [_]) => const SizedBox(), dispatchEvent: (UiEvent event) {}, buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), diff --git a/packages/genui/test/catalog/core_widgets_test.dart b/packages/genui/test/catalog/core_widgets_test.dart index 14bbff411..24c6a7fdb 100644 --- a/packages/genui/test/catalog/core_widgets_test.dart +++ b/packages/genui/test/catalog/core_widgets_test.dart @@ -7,11 +7,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; void main() { - group('Core Widgets', () { - final Catalog testCatalog = CoreCatalogItems.asCatalog(); + group('Basic Widgets', () { + final Catalog testCatalog = BasicCatalogItems.asCatalog(); ChatMessage? message; - A2uiMessageProcessor? messageProcessor; + SurfaceController? controller; Future pumpWidgetWithDefinition( WidgetTester tester, @@ -19,24 +19,20 @@ void main() { List components, ) async { message = null; - messageProcessor?.dispose(); - messageProcessor = A2uiMessageProcessor(catalogs: [testCatalog]); - messageProcessor!.onSubmit.listen((event) => message = event); + controller?.dispose(); + controller = SurfaceController(catalogs: [testCatalog]); + controller!.onSubmit.listen((event) => message = event); const surfaceId = 'testSurface'; - messageProcessor!.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + controller!.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), ); - messageProcessor!.handleMessage( - BeginRendering( - surfaceId: surfaceId, - root: rootId, - catalogId: testCatalog.catalogId, - ), + controller!.handleMessage( + CreateSurface(surfaceId: surfaceId, catalogId: testCatalog.catalogId!), ); await tester.pumpWidget( MaterialApp( home: Scaffold( - body: GenUiSurface(host: messageProcessor!, surfaceId: surfaceId), + body: Surface(surfaceContext: controller!.contextFor(surfaceId)), ), ), ); @@ -45,25 +41,23 @@ void main() { testWidgets('Button renders and handles taps', (WidgetTester tester) async { final components = [ const Component( - id: 'button', - componentProperties: { - 'Button': { - 'child': 'text', - 'action': {'name': 'testAction'}, + id: 'root', + type: 'Button', + properties: { + 'child': 'text', + 'action': { + 'event': {'name': 'testAction'}, }, }, ), const Component( id: 'text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Click Me'}, - }, - }, + type: 'Text', + properties: {'text': 'Click Me'}, ), ]; - await pumpWidgetWithDefinition(tester, 'button', components); + await pumpWidgetWithDefinition(tester, 'root', components); expect(find.text('Click Me'), findsOneWidget); @@ -75,18 +69,17 @@ void main() { testWidgets('Text renders from data model', (WidgetTester tester) async { final components = [ const Component( - id: 'text', - componentProperties: { - 'Text': { - 'text': {'path': '/myText'}, - }, + id: 'root', + type: 'Text', + properties: { + 'text': {'path': '/myText'}, }, ), ]; - await pumpWidgetWithDefinition(tester, 'text', components); - messageProcessor! - .dataModelForSurface('testSurface') + await pumpWidgetWithDefinition(tester, 'root', components); + controller!.store + .getDataModel('testSurface') .update(DataPath('/myText'), 'Hello from data model'); await tester.pumpAndSettle(); @@ -96,34 +89,25 @@ void main() { testWidgets('Column renders children', (WidgetTester tester) async { final components = [ const Component( - id: 'col', - componentProperties: { - 'Column': { - 'children': { - 'explicitList': ['text1', 'text2'], - }, - }, + id: 'root', + type: 'Column', + properties: { + 'children': ['text1', 'text2'], }, ), const Component( id: 'text1', - componentProperties: { - 'Text': { - 'text': {'literalString': 'First'}, - }, - }, + type: 'Text', + properties: {'text': 'First'}, ), const Component( id: 'text2', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Second'}, - }, - }, + type: 'Text', + properties: {'text': 'Second'}, ), ]; - await pumpWidgetWithDefinition(tester, 'col', components); + await pumpWidgetWithDefinition(tester, 'root', components); expect(find.text('First'), findsOneWidget); expect(find.text('Second'), findsOneWidget); @@ -134,20 +118,21 @@ void main() { ) async { final components = [ const Component( - id: 'field', - componentProperties: { - 'TextField': { - 'text': {'path': '/myValue'}, - 'label': {'literalString': 'My Label'}, - 'onSubmittedAction': {'name': 'submit'}, + id: 'root', + type: 'TextField', + properties: { + 'value': {'path': '/myValue'}, + 'label': 'My Label', + 'onSubmittedAction': { + 'event': {'name': 'submit'}, }, }, ), ]; await pumpWidgetWithDefinition(tester, 'field', components); - messageProcessor! - .dataModelForSurface('testSurface') + controller!.store + .getDataModel('testSurface') .update(DataPath('/myValue'), 'initial'); await tester.pumpAndSettle(); @@ -159,8 +144,8 @@ void main() { // Test onChanged await tester.enterText(textFieldFinder, 'new value'); expect( - messageProcessor! - .dataModelForSurface('testSurface') + controller!.store + .getDataModel('testSurface') .getValue(DataPath('/myValue')), 'new value', ); diff --git a/packages/genui/test/catalog_test.dart b/packages/genui/test/catalog_test.dart index 05469ad6a..41f98a99b 100644 --- a/packages/genui/test/catalog_test.dart +++ b/packages/genui/test/catalog_test.dart @@ -6,13 +6,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; -import 'package:logging/logging.dart'; void main() { group('Catalog', () { test('has a catalogId', () { final catalog = Catalog([ - CoreCatalogItems.text, + BasicCatalogItems.text, ], catalogId: 'test_catalog'); expect(catalog.catalogId, 'test_catalog'); }); @@ -20,13 +19,14 @@ void main() { testWidgets('buildWidget finds and builds the correct widget', ( WidgetTester tester, ) async { - final catalog = Catalog([CoreCatalogItems.column, CoreCatalogItems.text]); + final catalog = Catalog([ + BasicCatalogItems.column, + BasicCatalogItems.text, + ]); final widgetData = { - 'Column': { - 'children': { - 'explicitList': ['child1'], - }, - }, + 'children': [ + {'id': 'child1'}, + ], }; await tester.pumpWidget( @@ -37,6 +37,7 @@ void main() { return catalog.buildWidget( CatalogItemContext( id: 'col1', + type: 'Column', data: widgetData, buildChild: (_, [_]) => const Text(''), // Mock child builder @@ -44,6 +45,7 @@ void main() { buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surfaceId', ), ); @@ -57,56 +59,50 @@ void main() { expect(column.children.length, 1); }); - testWidgets('buildWidget returns empty container for unknown widget type', ( + testWidgets('buildWidget throws StateError for unknown widget type', ( WidgetTester tester, ) async { final catalog = const Catalog([]); final Map data = { 'id': 'text1', - 'widget': { - 'unknown_widget': {'text': 'hello'}, - }, + 'unknown_widget': {'text': 'hello'}, }; - final Future logFuture = expectLater( - genUiLogger.onRecord, - emits( - isA().having( - (e) => e.message, - 'message', - contains('Item unknown_widget was not found'), - ), - ), - ); await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (context) { - final Widget widget = catalog.buildWidget( - CatalogItemContext( - id: data['id'] as String, - data: data['widget'] as JsonMap, - buildChild: (_, [_]) => const SizedBox(), - dispatchEvent: (UiEvent event) {}, - buildContext: context, - dataContext: DataContext(DataModel(), '/'), - getComponent: (String componentId) => null, - surfaceId: 'surfaceId', + expect( + () => catalog.buildWidget( + CatalogItemContext( + id: 'text1', + type: 'unknown_widget', + data: data, + buildChild: (_, [_]) => const SizedBox(), + dispatchEvent: (UiEvent event) {}, + buildContext: context, + dataContext: DataContext(DataModel(), '/'), + getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, + surfaceId: 'surfaceId', + ), ), + throwsA(isA()), ); - expect(widget, isA()); - return widget; + return const SizedBox(); }, ), ), ), ); - await logFuture; }); test('schema generation is correct', () { - final catalog = Catalog([CoreCatalogItems.text, CoreCatalogItems.button]); + final catalog = Catalog([ + BasicCatalogItems.text, + BasicCatalogItems.button, + ]); final schema = catalog.definition as ObjectSchema; expect(schema.properties?.containsKey('components'), isTrue); diff --git a/packages/genui/test/core/a2ui_message_processor_test.dart b/packages/genui/test/core/a2ui_message_processor_test.dart deleted file mode 100644 index b60bab6ff..000000000 --- a/packages/genui/test/core/a2ui_message_processor_test.dart +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; - -void main() { - group('$A2uiMessageProcessor', () { - late A2uiMessageProcessor messageProcessor; - - setUp(() { - messageProcessor = A2uiMessageProcessor( - catalogs: [CoreCatalogItems.asCatalog()], - ); - }); - - tearDown(() { - messageProcessor.dispose(); - }); - - test('can be initialized with multiple catalogs', () { - final catalog1 = const Catalog([], catalogId: 'cat1'); - final catalog2 = const Catalog([], catalogId: 'cat2'); - final multiManager = A2uiMessageProcessor(catalogs: [catalog1, catalog2]); - expect(multiManager.catalogs, contains(catalog1)); - expect(multiManager.catalogs, contains(catalog2)); - expect(multiManager.catalogs.length, 2); - }); - - test('handleMessage adds a new surface and fires SurfaceAdded with ' - 'definition', () async { - const surfaceId = 's1'; - final components = [ - const Component( - id: 'root', - componentProperties: { - 'Text': {'text': 'Hello'}, - }, - ), - ]; - - messageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), - ); - - final Future futureUpdate = - messageProcessor.surfaceUpdates.first; - messageProcessor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'root', - catalogId: 'test_catalog', - ), - ); - final GenUiUpdate update = await futureUpdate; - - expect(update, isA()); - expect(update.surfaceId, surfaceId); - final UiDefinition definition = (update as SurfaceAdded).definition; - expect(definition, isNotNull); - expect(definition.rootComponentId, 'root'); - expect(definition.catalogId, 'test_catalog'); - expect(messageProcessor.surfaces[surfaceId]!.value, isNotNull); - expect( - messageProcessor.surfaces[surfaceId]!.value!.rootComponentId, - 'root', - ); - expect( - messageProcessor.surfaces[surfaceId]!.value!.catalogId, - 'test_catalog', - ); - }); - - test( - 'handleMessage updates an existing surface and fires SurfaceUpdated', - () async { - const surfaceId = 's1'; - final oldComponents = [ - const Component( - id: 'root', - componentProperties: { - 'Text': {'text': 'Old'}, - }, - ), - ]; - final newComponents = [ - const Component( - id: 'root', - componentProperties: { - 'Text': {'text': 'New'}, - }, - ), - ]; - - final Future expectation = expectLater( - messageProcessor.surfaceUpdates, - emitsInOrder([isA(), isA()]), - ); - - messageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: oldComponents), - ); - messageProcessor.handleMessage( - const BeginRendering(surfaceId: surfaceId, root: 'root'), - ); - messageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: newComponents), - ); - - await expectation; - }, - ); - - test('handleMessage removes a surface and fires SurfaceRemoved', () async { - const surfaceId = 's1'; - final components = [ - const Component( - id: 'root', - componentProperties: { - 'Text': {'text': 'Hello'}, - }, - ), - ]; - messageProcessor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), - ); - - final Future futureUpdate = - messageProcessor.surfaceUpdates.first; - messageProcessor.handleMessage( - const SurfaceDeletion(surfaceId: surfaceId), - ); - final GenUiUpdate update = await futureUpdate; - - expect(update, isA()); - expect(update.surfaceId, surfaceId); - expect(messageProcessor.surfaces.containsKey(surfaceId), isFalse); - }); - - test('surface() creates a new ValueNotifier if one does not exist', () { - final ValueNotifier notifier1 = messageProcessor - .getSurfaceNotifier('s1'); - final ValueNotifier notifier2 = messageProcessor - .getSurfaceNotifier('s1'); - expect(notifier1, same(notifier2)); - expect(notifier1.value, isNull); - }); - - test('dispose() closes the updates stream', () async { - var isClosed = false; - messageProcessor.surfaceUpdates.listen( - null, - onDone: () { - isClosed = true; - }, - ); - - messageProcessor.dispose(); - - await Future.delayed(Duration.zero); - expect(isClosed, isTrue); - }); - - test('can handle UI event', () async { - messageProcessor - .dataModelForSurface('testSurface') - .update(DataPath('/myValue'), 'testValue'); - final Future future = - messageProcessor.onSubmit.first; - final now = DateTime.now(); - final event = UserActionEvent( - surfaceId: 'testSurface', - name: 'testAction', - sourceComponentId: 'testWidget', - timestamp: now, - context: {'key': 'value'}, - ); - messageProcessor.handleUiEvent(event); - final UserUiInteractionMessage message = await future; - expect(message, isA()); - final String expectedJson = jsonEncode({ - 'userAction': { - 'surfaceId': 'testSurface', - 'name': 'testAction', - 'sourceComponentId': 'testWidget', - 'timestamp': now.toIso8601String(), - 'isAction': true, - 'context': {'key': 'value'}, - }, - }); - expect(message.text, expectedJson); - }); - }); -} diff --git a/packages/genui/test/core/a2ui_message_processor_validation_test.dart b/packages/genui/test/core/a2ui_message_processor_validation_test.dart new file mode 100644 index 000000000..6986e425c --- /dev/null +++ b/packages/genui/test/core/a2ui_message_processor_validation_test.dart @@ -0,0 +1,37 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +void main() { + group('SurfaceController Validation', () { + test('CreateSurface fails validation with empty surfaceId', () async { + final controller = SurfaceController(catalogs: []); + + // Expect an error message on the submit stream + final Future future = expectLater( + controller.onSubmit, + emits( + predicate((ChatMessage message) { + final UiInteractionPart part = + message.parts.uiInteractionParts.first; + final json = jsonDecode(part.interaction) as Map; + final error = json['error'] as Map; + return error['code'] == 'VALIDATION_FAILED' && + error['path'] == 'surfaceId'; + }), + ), + ); + + controller.handleMessage( + const CreateSurface(surfaceId: '', catalogId: 'default'), + ); + + await future; + }); + }); +} diff --git a/packages/genui/test/core/surface_widget_test.dart b/packages/genui/test/core/surface_widget_test.dart index 6e27b128a..62b8ccbd5 100644 --- a/packages/genui/test/core/surface_widget_test.dart +++ b/packages/genui/test/core/surface_widget_test.dart @@ -2,10 +2,202 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:genui/src/catalog/basic_catalog_widgets/text.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +class FakeSurfaceContext implements SurfaceContext { + FakeSurfaceContext({ + required this.surfaceId, + required this.dataModel, + required this.catalogs, + required this.definition, + required this.handleUiEventCallback, + this.reportErrorCallback, + }); + + @override + final String surfaceId; + + @override + final DataModel dataModel; + + @override + final Iterable catalogs; + + @override + final ValueNotifier definition; + + final void Function(UiEvent) handleUiEventCallback; + + @override + void handleUiEvent(UiEvent event) { + handleUiEventCallback(event); + } + + final void Function(Object error, StackTrace? stack)? reportErrorCallback; + + @override + void reportError(Object error, StackTrace? stack) { + if (reportErrorCallback != null) { + reportErrorCallback!(error, stack); + } + } +} void main() { group('SurfaceWidget', () { - // TODO(gspencer): Write tests for SurfaceWidget. + late DataModel dataModel; + late FakeSurfaceContext surfaceContext; + late Catalog catalog; + + setUp(() { + dataModel = DataModel(); + catalog = Catalog([text], catalogId: 'test_catalog'); + surfaceContext = FakeSurfaceContext( + surfaceId: 'test_surface', + dataModel: dataModel, + catalogs: [catalog], + definition: ValueNotifier(null), + handleUiEventCallback: (event) {}, + ); + }); + + testWidgets('renders empty when no definition', (tester) async { + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: surfaceContext)), + ); + + expect(find.byType(SizedBox), findsOneWidget); + }); + + testWidgets('renders default builder when no definition', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Surface( + surfaceContext: surfaceContext, + defaultBuilder: (context) => const Text('Loading...'), + ), + ), + ); + + expect(find.text('Loading...'), findsOneWidget); + }); + + testWidgets('renders root component', (tester) async { + surfaceContext.definition.value = SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'root': const Component( + id: 'root', + type: 'Text', + properties: {'text': 'Hello World'}, + ), + }, + ); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: surfaceContext)), + ); + + expect(find.text('Hello World'), findsOneWidget); + }); + + testWidgets('handles missing root component', (tester) async { + surfaceContext.definition.value = SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'other': const Component( + id: 'other', + type: 'Text', + properties: {'text': 'Hidden'}, + ), + }, + ); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: surfaceContext)), + ); + + expect(find.text('Hidden'), findsNothing); + expect(find.byType(SizedBox), findsOneWidget); + }); + + testWidgets('propogates data context', (tester) async { + dataModel.update(DataPath('/message'), 'Dynamic Content'); + surfaceContext.definition.value = SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'root': const Component( + id: 'root', + type: 'Text', + properties: { + 'text': {'path': '/message'}, + }, + ), + }, + ); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: surfaceContext)), + ); + + expect(find.text('Dynamic Content'), findsOneWidget); + + // Update data + dataModel.update(DataPath('/message'), 'Updated Content'); + await tester.pump(); + + expect(find.text('Updated Content'), findsOneWidget); + }); + + testWidgets('reports error and shows fallback when builder throws', ( + tester, + ) async { + Object? reportedError; + dataModel = DataModel(); + surfaceContext = FakeSurfaceContext( + surfaceId: 'test_surface', + dataModel: dataModel, + catalogs: [ + Catalog([ + CatalogItem( + name: 'BrokenWidget', + dataSchema: Schema.object(properties: {}), + widgetBuilder: (context) => throw Exception('Build failed'), + ), + ], catalogId: 'test_catalog'), + ], + definition: ValueNotifier( + SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'root': const Component( + id: 'root', + type: 'BrokenWidget', + properties: {}, + ), + }, + ), + ), + handleUiEventCallback: (event) {}, + reportErrorCallback: (error, stack) { + reportedError = error; + }, + ); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: surfaceContext)), + ); + + expect(reportedError, isNotNull); + expect(find.byType(FallbackWidget), findsOneWidget); + }); }); } diff --git a/packages/genui/test/core/ui_tools_test.dart b/packages/genui/test/core/ui_tools_test.dart deleted file mode 100644 index 0893054d8..000000000 --- a/packages/genui/test/core/ui_tools_test.dart +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/src/core/ui_tools.dart'; -import 'package:genui/src/model/a2ui_message.dart'; -import 'package:genui/src/model/catalog.dart'; -import 'package:genui/src/model/catalog_item.dart'; -import 'package:genui/src/model/tools.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -void main() { - group('$SurfaceUpdateTool', () { - test('invoke calls handleMessage with correct arguments', () async { - final messages = []; - - void fakeHandleMessage(A2uiMessage message) { - messages.add(message); - } - - final tool = SurfaceUpdateTool( - handleMessage: fakeHandleMessage, - catalog: Catalog([ - CatalogItem( - name: 'Text', - widgetBuilder: (_) { - return const Text(''); - }, - dataSchema: Schema.object(properties: {}), - ), - ], catalogId: 'test_catalog'), - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'components': [ - { - 'id': 'rootWidget', - 'component': { - 'Text': {'text': 'Hello'}, - }, - }, - ], - }; - - await tool.invoke(args); - - expect(messages.length, 1); - expect(messages[0], isA()); - final surfaceUpdate = messages[0] as SurfaceUpdate; - expect(surfaceUpdate.surfaceId, 'testSurface'); - expect(surfaceUpdate.components.length, 1); - expect(surfaceUpdate.components[0].id, 'rootWidget'); - expect(surfaceUpdate.components[0].componentProperties, { - 'Text': {'text': 'Hello'}, - }); - expect(surfaceUpdate.components[0].weight, isNull); - }); - - test('invoke correctly parses int weight', () async { - final messages = []; - final tool = SurfaceUpdateTool( - handleMessage: messages.add, - catalog: const Catalog([], catalogId: 'test_catalog'), - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'components': [ - { - 'id': 'weightedWidget', - 'component': {'Text': {}}, - 'weight': 1, - }, - ], - }; - - await tool.invoke(args); - - expect(messages.length, 1); - final surfaceUpdate = messages[0] as SurfaceUpdate; - expect(surfaceUpdate.components[0].weight, 1); - }); - - test('invoke correctly parses double weight', () async { - final messages = []; - final tool = SurfaceUpdateTool( - handleMessage: messages.add, - catalog: const Catalog([], catalogId: 'test_catalog'), - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'components': [ - { - 'id': 'weightedWidget', - 'component': {'Text': {}}, - 'weight': 1.0, - }, - ], - }; - - await tool.invoke(args); - - expect(messages.length, 1); - final surfaceUpdate = messages[0] as SurfaceUpdate; - expect(surfaceUpdate.components[0].weight, 1); - }); - }); - - group('DeleteSurfaceTool', () { - test('invoke calls handleMessage with correct arguments', () async { - final messages = []; - - void fakeHandleMessage(A2uiMessage message) { - messages.add(message); - } - - final tool = DeleteSurfaceTool(handleMessage: fakeHandleMessage); - - final Map args = {surfaceIdKey: 'testSurface'}; - - await tool.invoke(args); - - expect(messages.length, 1); - expect(messages[0], isA()); - final deleteSurface = messages[0] as SurfaceDeletion; - expect(deleteSurface.surfaceId, 'testSurface'); - }); - }); - - group('BeginRenderingTool', () { - test('invoke calls handleMessage with correct arguments', () async { - final messages = []; - - void fakeHandleMessage(A2uiMessage message) { - messages.add(message); - } - - final tool = BeginRenderingTool( - handleMessage: fakeHandleMessage, - catalogId: 'test_catalog', - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'root': 'rootWidget', - }; - - await tool.invoke(args); - - expect(messages.length, 1); - expect(messages[0], isA()); - final beginRendering = messages[0] as BeginRendering; - expect(beginRendering.surfaceId, 'testSurface'); - expect(beginRendering.root, 'rootWidget'); - expect(beginRendering.catalogId, 'test_catalog'); - }); - }); -} diff --git a/packages/genui/test/core_catalog_validation_test.dart b/packages/genui/test/core_catalog_validation_test.dart index 271f1f428..3c0010a51 100644 --- a/packages/genui/test/core_catalog_validation_test.dart +++ b/packages/genui/test/core_catalog_validation_test.dart @@ -7,8 +7,8 @@ import 'package:genui/genui.dart'; import 'package:genui/test.dart'; void main() { - group('Core Catalog Validation', () { - final Catalog mergedCatalog = CoreCatalogItems.asCatalog(); + group('Basic Catalog Validation', () { + final Catalog mergedCatalog = BasicCatalogItems.asCatalog(); for (final CatalogItem item in mergedCatalog.items) { test('CatalogItem ${item.name} examples are valid', () async { diff --git a/packages/genui/test/development_utilities/catalog_view_test.dart b/packages/genui/test/development_utilities/catalog_view_test.dart index 70cc62159..29f6b0ad5 100644 --- a/packages/genui/test/development_utilities/catalog_view_test.dart +++ b/packages/genui/test/development_utilities/catalog_view_test.dart @@ -32,18 +32,11 @@ CatalogItem getCatalogItemForTesting(String successMessage) { final catalogItemName = 'TextForTesting'; return CatalogItem( name: catalogItemName, - dataSchema: CoreCatalogItems.text.dataSchema, - widgetBuilder: CoreCatalogItems.text.widgetBuilder, + dataSchema: BasicCatalogItems.text.dataSchema, + widgetBuilder: BasicCatalogItems.text.widgetBuilder, exampleData: [ () => jsonEncode([ - { - 'id': 'root', - 'component': { - catalogItemName: { - 'text': {'literalString': successMessage}, - }, - }, - }, + {'id': 'root', 'component': catalogItemName, 'text': successMessage}, ]), ], ); diff --git a/packages/genui/test/engine/surface_controller_test.dart b/packages/genui/test/engine/surface_controller_test.dart new file mode 100644 index 000000000..88e0bc588 --- /dev/null +++ b/packages/genui/test/engine/surface_controller_test.dart @@ -0,0 +1,337 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; + +void main() { + group('$SurfaceController', () { + late SurfaceController controller; + + setUp(() { + controller = SurfaceController(catalogs: [BasicCatalogItems.asCatalog()]); + }); + + tearDown(() { + controller.dispose(); + }); + + test('can be initialized with multiple catalogs', () { + final catalog1 = const Catalog([], catalogId: 'cat1'); + final catalog2 = const Catalog([], catalogId: 'cat2'); + final multiManager = SurfaceController(catalogs: [catalog1, catalog2]); + expect(multiManager.catalogs, contains(catalog1)); + expect(multiManager.catalogs, contains(catalog2)); + expect(multiManager.catalogs.length, 2); + }); + + test('handleMessage adds a new surface and fires SurfaceAdded with ' + 'definition', () async { + const surfaceId = 's1'; + final components = [ + const Component( + id: 'root', + type: 'Text', + properties: {'text': 'Hello'}, + ), + ]; + + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + + final Future> futureUpdates = controller + .surfaceUpdates + .take(2) + .toList(); + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + final List updates = await futureUpdates; + + expect(updates[0], isA()); + expect(updates[0].surfaceId, surfaceId); + + final SurfaceUpdate update2 = updates[1]; + expect(update2, isA()); + final SurfaceDefinition definition = + (update2 as ComponentsUpdated).definition; + + expect(definition, isNotNull); + expect( + definition.components['root'], + isNotNull, + ); // Check if root (or any component) exists + expect(definition.catalogId, 'test_catalog'); + expect(controller.registry.getSurface(surfaceId), isNotNull); + expect( + controller.registry.getSurface(surfaceId)!.catalogId, + 'test_catalog', + ); + }); + + test( + 'handleMessage updates an existing surface and fires ComponentsUpdated', + () async { + const surfaceId = 's1'; + final oldComponents = [ + const Component( + id: 'root', + type: 'Text', + properties: {'text': 'Old'}, + ), + ]; + final newComponents = [ + const Component( + id: 'root', + type: 'Text', + properties: {'text': 'New'}, + ), + ]; + + final Future expectation = expectLater( + controller.surfaceUpdates, + emitsInOrder([ + isA(), + isA(), + isA(), + ]), + ); + + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: oldComponents), + ); + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: newComponents), + ); + + await expectation; + }, + ); + + test('handleMessage removes a surface and fires SurfaceRemoved', () async { + const surfaceId = 's1'; + final components = [ + const Component( + id: 'root', + type: 'Text', + properties: {'text': 'Hello'}, + ), + ]; + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + + final Future futureUpdate = + controller.surfaceUpdates.first; + + controller.handleMessage(const DeleteSurface(surfaceId: surfaceId)); + final SurfaceUpdate update = await futureUpdate; + + expect(update, isA()); + expect(update.surfaceId, surfaceId); + expect(controller.registry.hasSurface(surfaceId), isFalse); + }); + + test('surface() creates a new ValueNotifier if one does not exist', () { + final ValueListenable notifier1 = controller.registry + .watchSurface('s1'); + final ValueListenable notifier2 = controller.registry + .watchSurface('s1'); + expect(notifier1, same(notifier2)); + expect(notifier1.value, isNull); + }); + + test('dispose() closes the updates stream', () async { + var isClosed = false; + controller.surfaceUpdates.listen( + null, + onDone: () { + isClosed = true; + }, + ); + + controller.dispose(); + + await Future.delayed(Duration.zero); + expect(isClosed, isTrue); + }); + + test('can handle UI event', () async { + controller.store + .getDataModel('testSurface') + .update(DataPath('/myValue'), 'testValue'); + final Future future = controller.onSubmit.first; + final now = DateTime.now(); + final event = UserActionEvent( + surfaceId: 'testSurface', + name: 'testAction', + sourceComponentId: 'testWidget', + timestamp: now, + context: {'key': 'value'}, + ); + controller.handleUiEvent(event); + final ChatMessage message = await future; + expect(message, isA()); + expect(message.role, ChatMessageRole.user); + expect(message.parts.uiInteractionParts, hasLength(1)); + + final String expectedJson = jsonEncode({ + 'version': 'v0.9', + 'action': { + 'surfaceId': 'testSurface', + 'name': 'testAction', + 'sourceComponentId': 'testWidget', + 'timestamp': now.toIso8601String(), + 'context': {'key': 'value'}, + }, + }); + final UiInteractionPart part = message.parts.uiInteractionParts.first; + // Depending on implementation, part.interaction might be the string or + // data map. UiInteractionPart.create took jsonEncode string. + // UiInteractionPart.interaction is String. + expect(part.interaction, expectedJson); + }); + + test( + 'handleMessage reports validation error with correct structure', + () async { + // Trigger validation error by using an empty surface ID. + final Future messageFuture = controller.onSubmit.first; + controller.handleMessage( + const CreateSurface(surfaceId: '', catalogId: 'test_catalog'), + ); + + final ChatMessage message = await messageFuture; + expect(message.role, ChatMessageRole.user); + final UiInteractionPart part = message.parts.uiInteractionParts.first; + final errorJson = jsonDecode(part.interaction) as Map; + + expect(errorJson['version'], 'v0.9'); + final Object? errorObj = errorJson['error']; + expect(errorObj, isA>()); + final errorMap = errorObj! as Map; + expect(errorMap['code'], 'VALIDATION_FAILED'); + expect(errorMap['surfaceId'], ''); + expect(errorMap['path'], 'surfaceId'); + }, + ); + + test('drops pending updates after timeout', () async { + // Create controller with short timeout + final shortTimeoutController = SurfaceController( + catalogs: [BasicCatalogItems.asCatalog()], + pendingUpdateTimeout: const Duration(milliseconds: 100), + ); + addTearDown(shortTimeoutController.dispose); + + const surfaceId = 'timedOutSurface'; + final components = [ + const Component( + id: 'root', + type: 'Text', + properties: {'text': 'Should not be seen'}, + ), + ]; + + // 1. Send update for non-existent surface (buffered) + shortTimeoutController.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), + ); + + // 2. Wait for timeout + await Future.delayed(const Duration(milliseconds: 200)); + + // 3. Create surface (but first setup listener) + final Future> updatesFuture = shortTimeoutController + .surfaceUpdates + .take(1) + .toList(); + shortTimeoutController.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), + ); + + // 4. Verify surface created but NO update applied + // If update was applied, we'd see [SurfaceAdded, ComponentsUpdated] + // If dropped, we only see [SurfaceAdded] (and potentially components from + // CreateSurface if any, but default is empty) + final List updates = await updatesFuture; + expect(updates.length, 1); + expect(updates[0], isA()); + + // Allow a small delay to ensure no other events come through + // Testing emptiness of a stream is tricky, checking registry state is + // better. + await Future.delayed(Duration.zero); + + final SurfaceDefinition? surface = shortTimeoutController.registry + .getSurface(surfaceId); + expect(surface, isNotNull); + // Updates NOT applied, so components should be empty (or default) + expect(surface!.components, isEmpty); + }); + + test( + 'handleMessage reports schema validation error for invalid component', + () async { + final catalog = Catalog([ + CatalogItem( + name: 'StrictWidget', + dataSchema: Schema.object( + properties: { + 'component': Schema.string(enumValues: ['StrictWidget']), + 'requiredProp': Schema.string(), + }, + required: ['component', 'requiredProp'], + ), + widgetBuilder: _dummyBuilder, + ), + ], catalogId: 'strict_catalog'); + final strictController = SurfaceController(catalogs: [catalog]); + addTearDown(strictController.dispose); + + const surfaceId = 'strictSurface'; + strictController.handleMessage( + const CreateSurface( + surfaceId: surfaceId, + catalogId: 'strict_catalog', + ), + ); + + final Future future = strictController.onSubmit.first; + + // Send invalid component (missing requiredProp) + strictController.handleMessage( + const UpdateComponents( + surfaceId: surfaceId, + components: [ + Component(id: 'bad', type: 'StrictWidget', properties: {}), + ], + ), + ); + + final ChatMessage message = await future; + final UiInteractionPart part = message.parts.uiInteractionParts.first; + final errorJson = jsonDecode(part.interaction) as Map; + + final errorObj = errorJson['error'] as Map; + expect(errorObj['code'], 'VALIDATION_FAILED'); + expect(errorObj['message'], contains('Missing required property')); + }, + ); + }); +} + +Widget _dummyBuilder(CatalogItemContext context) => const SizedBox(); diff --git a/packages/genui/test/error_reporting_test.dart b/packages/genui/test/error_reporting_test.dart new file mode 100644 index 000000000..34830fd4e --- /dev/null +++ b/packages/genui/test/error_reporting_test.dart @@ -0,0 +1,186 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +import 'package:genui/src/functions/functions.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; + +void main() { + group('Error Reporting Tests', () { + late Logger logger; + late List logs; + + setUp(() { + logs = []; + hierarchicalLoggingEnabled = true; + logger = Logger('genui'); + logger.level = Level.ALL; + logger.onRecord.listen((record) => logs.add(record)); + }); + + test( + 'A2uiMessage.fromJson throws A2uiValidationException for unknown message ' + 'type', + () { + final json = { + 'version': 'v0.9', + 'unknownAction': {}, + }; + + try { + A2uiMessage.fromJson(json); + fail('Should have thrown A2uiValidationException'); + } on A2uiValidationException catch (e) { + expect(e.message, contains('Unknown A2UI message type')); + } + }, + ); + + testWidgets('Surface reports error when catalog item is missing', ( + tester, + ) async { + final dataModel = DataModel(); + final context = MockSurfaceContext('test_surface', dataModel); + + final definition = SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'root': const Component( + id: 'root', + type: 'NonExistentComponent', + properties: {}, + ), + }, + ); + + context.updateDefinition(definition); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: context)), + ); + + // Should show FallbackWidget + expect(find.byType(FallbackWidget), findsOneWidget); + expect(context.reportedErrors, isNotEmpty); + expect( + context.reportedErrors.first.toString(), + contains('NonExistentComponent'), + ); + }); + + testWidgets('Surface reports error when FunctionRegistry throws', ( + tester, + ) async { + // Register a failing function + FunctionRegistry().register('failFunc', (args) { + throw Exception('Function failed explicitly'); + }); + + final dataModel = DataModel(); + final context = MockSurfaceContext('test_surface', dataModel); + + final definition = SurfaceDefinition( + surfaceId: 'test_surface', + catalogId: 'test_catalog', + components: { + 'root': const Component( + id: 'root', + type: 'Text', + properties: { + 'text': {'call': 'failFunc', 'args': {}}, + }, + ), + }, + ); + + context.updateDefinition(definition); + + await tester.pumpWidget( + MaterialApp(home: Surface(surfaceContext: context)), + ); + + // Verify FallbackWidget and reported error + expect(find.byType(FallbackWidget), findsOneWidget); + expect(context.reportedErrors, isNotEmpty); + expect( + context.reportedErrors.first.toString(), + contains('Function failed explicitly'), + ); + }); + + testWidgets('FunctionRegistry._regex throws on invalid regex', ( + tester, + ) async { + // We can test this directly via FunctionRegistry + // 'regex' function: { 'value': 'foo', 'pattern': '(' } -> Should throw + + final registry = FunctionRegistry(); + try { + registry.invoke('regex', {'value': 'foo', 'pattern': '('}); + fail('Should have thrown FormatException or similar'); + } catch (e) { + expect(e.toString(), contains('Invalid regex pattern')); + } + }); + }); +} + +class MockSurfaceContext implements SurfaceContext { + MockSurfaceContext(this.surfaceId, this.dataModel); + + @override + final String surfaceId; + + @override + final DataModel dataModel; + + final ValueNotifier _definition = ValueNotifier(null); + + @override + ValueListenable get definition => _definition; + + void updateDefinition(SurfaceDefinition def) { + _definition.value = def; + } + + @override + List get catalogs => [_testCatalog]; + + final List reportedErrors = []; + + @override + void reportError(Object error, StackTrace? stack) { + reportedErrors.add(error); + } + + @override + void handleUiEvent(UiEvent event) {} +} + +// Minimal catalog for testing +final Catalog _testCatalog = Catalog(catalogId: 'test_catalog', [ + CatalogItem( + name: 'Text', + dataSchema: S.object(), + widgetBuilder: (ctx) { + try { + final Object? text = (ctx.data as Map?)?['text']; + if (text is Map && text.containsKey('call')) { + FunctionRegistry().invoke( + text['call'] as String, + (text['args'] as Map?) ?? {}, + ); + } + } catch (e) { + rethrow; + } + return const Text('Placeholder'); + }, + ), +]); diff --git a/packages/genui/test/facade/conversation_test.dart b/packages/genui/test/facade/conversation_test.dart new file mode 100644 index 000000000..5037ed568 --- /dev/null +++ b/packages/genui/test/facade/conversation_test.dart @@ -0,0 +1,82 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +void main() { + group('Conversation', () { + late A2uiTransportAdapter adapter; + late SurfaceController controller; + + setUp(() { + adapter = A2uiTransportAdapter(); + controller = SurfaceController(catalogs: []); + }); + + tearDown(() { + adapter.dispose(); + controller.dispose(); + }); + + test('updates isWaiting state during request', () async { + final completer = Completer(); + adapter = A2uiTransportAdapter( + onSend: (message) async { + await completer.future; + }, + ); + + final conversation = Conversation( + transport: adapter, + controller: controller, + ); + + expect(conversation.state.value.isWaiting, isFalse); + + final Future future = conversation.sendRequest( + ChatMessage.user('hi', parts: [UiInteractionPart.create('hi')]), + ); + + expect(conversation.state.value.isWaiting, isTrue); + + completer.complete(); + await future; + + expect(conversation.state.value.isWaiting, isFalse); + conversation.dispose(); + }); + + test('calls onSend with correct message', () async { + ChatMessage? capturedMessage; + + adapter = A2uiTransportAdapter( + onSend: (message) async { + capturedMessage = message; + }, + ); + + final conversation = Conversation( + transport: adapter, + controller: controller, + ); + + // Send first message + final firstMessage = ChatMessage.user('First'); + await conversation.sendRequest(firstMessage); + + expect(capturedMessage, firstMessage); + + // Send second message + final secondMessage = ChatMessage.user('Second'); + await conversation.sendRequest(secondMessage); + + expect(capturedMessage, secondMessage); + + conversation.dispose(); + }); + }); +} diff --git a/packages/genui/test/facade/prompt_builder_test.dart b/packages/genui/test/facade/prompt_builder_test.dart new file mode 100644 index 000000000..0d5c67f20 --- /dev/null +++ b/packages/genui/test/facade/prompt_builder_test.dart @@ -0,0 +1,51 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; + +void main() { + group('PromptBuilder', () { + const instructions = 'These are some instructions.'; + final catalog = const Catalog([]); // Empty catalog for testing. + + test('includes instructions when provided', () { + final builder = PromptBuilder.chat( + catalog: catalog, + instructions: instructions, + ); + + expect(builder.systemPrompt, contains(instructions)); + }); + + test('includes warning about surfaceId', () { + final builder = PromptBuilder.chat(catalog: catalog); + + expect(builder.systemPrompt, contains('IMPORTANT: When you generate UI')); + expect(builder.systemPrompt, contains('surfaceId')); + }); + + test('includes A2UI schema', () { + final builder = PromptBuilder.chat(catalog: catalog); + + expect(builder.systemPrompt, contains('')); + expect(builder.systemPrompt, contains('')); + }); + + test('includes basic catalog rules', () { + final builder = PromptBuilder.chat(catalog: catalog); + + expect( + builder.systemPrompt, + contains(BasicCatalogEmbed.basicCatalogRules), + ); + }); + + test('includes basic chat prompt fragment', () { + final builder = PromptBuilder.chat(catalog: catalog); + + expect(builder.systemPrompt, contains('# Outputting UI information')); + }); + }); +} diff --git a/packages/genui/test/functions/expression_parser_test.dart b/packages/genui/test/functions/expression_parser_test.dart new file mode 100644 index 000000000..911425cef --- /dev/null +++ b/packages/genui/test/functions/expression_parser_test.dart @@ -0,0 +1,263 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/functions/expression_parser.dart'; +import 'package:genui/src/model/data_model.dart'; + +void main() { + group('ExpressionParser', () { + late DataContext context; + late ExpressionParser parser; + late DataModel dataModel; + + setUp(() { + dataModel = DataModel(); + context = DataContext(dataModel, '/'); + parser = ExpressionParser(context); + }); + + group('parse', () { + test('returns input if no expressions', () { + expect(parser.parse('hello'), 'hello'); + expect(parser.parse('123'), '123'); + }); + + test('resolves simple path expression', () { + dataModel.update(DataPath('/name'), 'World'); + expect(parser.parse(r'Hello ${/name}'), 'Hello World'); + }); + + test('resolves multiple expressions', () { + dataModel.update(DataPath('/firstName'), 'John'); + dataModel.update(DataPath('/lastName'), 'Doe'); + expect(parser.parse(r'${/firstName} ${/lastName}'), 'John Doe'); + }); + + test('escapes expression', () { + expect(parser.parse(r'Value: \${/foo}'), r'Value: ${/foo}'); + }); + + test('returns non-string type for single expression', () { + dataModel.update(DataPath('/count'), 42); + expect(parser.parse(r'${/count}'), 42); + }); + + test('converts non-string values to string when mixed with text', () { + dataModel.update(DataPath('/count'), 42); + expect(parser.parse(r'Count: ${/count}'), 'Count: 42'); + }); + }); + + group('evaluateFunctionCall (Logic)', () { + test('and', () { + expect( + parser.evaluateFunctionCall({ + 'call': 'and', + 'args': { + 'values': [true, true], + }, + }), + isTrue, + ); + expect( + parser.evaluateFunctionCall({ + 'call': 'and', + 'args': { + 'values': [true, false], + }, + }), + isFalse, + ); + }); + + test('or', () { + expect( + parser.evaluateFunctionCall({ + 'call': 'or', + 'args': { + 'values': [false, true], + }, + }), + isTrue, + ); + expect( + parser.evaluateFunctionCall({ + 'call': 'or', + 'args': { + 'values': [false, false], + }, + }), + isFalse, + ); + }); + + test('not', () { + expect( + parser.evaluateFunctionCall({ + 'call': 'not', + 'args': {'value': true}, + }), + isFalse, + ); + expect( + parser.evaluateFunctionCall({ + 'call': 'not', + 'args': {'value': false}, + }), + isTrue, + ); + }); + + test('standard function', () { + // 'required' is a standard function + expect( + parser.evaluateFunctionCall({ + 'call': 'required', + 'args': {'value': 'something'}, + }), + isTrue, + ); + expect( + parser.evaluateFunctionCall({ + 'call': 'required', + 'args': {'value': ''}, + }), + isFalse, + ); + }); + + test('nested function calls', () { + // not(and(true, false)) -> not(false) -> true + expect( + parser.evaluateFunctionCall({ + 'call': 'not', + 'args': { + 'value': { + 'call': 'and', + 'args': { + 'values': [true, false], + }, + }, + }, + }), + isTrue, + ); + }); + }); + + group('Function calls in expressions', () { + test('resolves simple function', () { + expect(parser.parse(r'${formatString(value: "Hello")}'), 'Hello'); + }); + + test('resolves nested function', () { + expect( + parser.parse( + r'${formatString(value: ${formatString(value: "Nested")})}', + ), + 'Nested', + ); + }); + + test('resolves function with path args', () { + dataModel.update(DataPath('/val'), 'Dynamic'); + expect(parser.parse(r'${formatString(value: ${/val})}'), 'Dynamic'); + }); + + test('resolves function with quoted string containing spaces', () { + expect( + parser.parse(r'${formatString(value: "Hello World")}'), + 'Hello World', + ); + }); + }); + + group('extractDependencies', () { + test('returns empty for no expressions', () { + expect(parser.extractDependencies('hello'), isEmpty); + }); + + test('returns single path', () { + expect(parser.extractDependencies(r'${/foo}'), {DataPath('/foo')}); + }); + + test('returns multiple paths', () { + expect(parser.extractDependencies(r'${/foo} ${/bar}'), { + DataPath('/foo'), + DataPath('/bar'), + }); + }); + + test('returns paths in function calls', () { + expect(parser.extractDependencies(r'${formatString(value: ${/foo})}'), { + DataPath('/foo'), + }); + }); + + test('returns paths in nested function calls', () { + expect( + parser.extractDependencies( + r'${upper(value: ${lower(value: ${/foo})})}', + ), + {DataPath('/foo')}, + ); + }); + + test('returns paths in nested interpolations', () { + expect(parser.extractDependencies(r'${foo(val: ${/bar})}'), { + DataPath('/bar'), + }); + }); + + test('returns paths with whitespace', () { + expect(parser.extractDependencies(r'${ /foo }'), {DataPath('/foo')}); + }); + + test('returns paths with mixed content', () { + expect(parser.extractDependencies(r'Value: ${/foo}, Count: ${/bar}'), { + DataPath('/foo'), + DataPath('/bar'), + }); + }); + }); + + group('Invalid syntax', () { + test('rejects function call with raw function call as argument', () { + // ${foo(bar())} should NOT parse bar() as a function call. + // It should be treated as a path "bar()" which likely resolves to + // null, or fail to parse the outer function call due to syntax error + // (missing colon). + // + // "bar()" is not a valid named argument key (missing colon). + // So _parseNamedArgs will likely return empty map or partial map. + // "foo" will be called with empty/partial args. + // + // Verify that "bar" is NOT invoked. + parser = ExpressionParser(context); + + // ${not(true)} -> false. + // ${not(not(false))} -> true (if nested). + // ${not(not(false))} -> invalid syntax "not(false)" is not + // "key: value". + expect(parser.parse(r'${not(not(false))}'), isNot(true)); + }); + + test( + 'rejects function call with raw function call as named argument value', + () { + // ${not(value: not(value: false))} + // Inner "not(value: false)" is NOT a string literal or ${...}. + // It is treated as a path "not(value: false)". + // So outer not receives "value": null (result of path lookup). + // not(null) -> true. + // But if it was parsed as function, not(true) -> false. + // So if it returns true, it means it FAILED to parse inner + // function. + expect(parser.parse(r'${not(value: not(value: false))}'), true); + }, + ); + }); + }); +} diff --git a/packages/genui/test/functions/functions_test.dart b/packages/genui/test/functions/functions_test.dart new file mode 100644 index 000000000..534443d0d --- /dev/null +++ b/packages/genui/test/functions/functions_test.dart @@ -0,0 +1,190 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/functions/functions.dart'; + +void main() { + group('FunctionRegistry', () { + late FunctionRegistry registry; + + setUp(() { + // FunctionRegistry is a singleton, so we're testing the shared instance + registry = FunctionRegistry(); + }); + + test('register and invoke custom function', () { + registry.register('customFunc', (args) => 'invoked with ${args['val']}'); + expect(registry.invoke('customFunc', {'val': 1}), 'invoked with 1'); + }); + + test('returns null for unknown function', () { + expect(registry.invoke('unknownFunc', {}), isNull); + }); + + group('Standard Functions', () { + test('required', () { + expect(registry.invoke('required', {}), isFalse); + expect(registry.invoke('required', {'value': null}), isFalse); + expect(registry.invoke('required', {'value': ''}), isFalse); + expect(registry.invoke('required', {'value': []}), isFalse); + expect(registry.invoke('required', {'value': {}}), isFalse); + expect(registry.invoke('required', {'value': 'valid'}), isTrue); + expect( + registry.invoke('required', { + 'value': [1], + }), + isTrue, + ); + expect( + registry.invoke('required', { + 'value': {'k': 'v'}, + }), + isTrue, + ); + }); + + test('regex', () { + expect( + registry.invoke('regex', { + 'value': 'test@example.com', + 'pattern': r'^[^@]+@[^@]+\.[^@]+$', + }), + isTrue, + ); + expect( + registry.invoke('regex', { + 'value': 'invalid', + 'pattern': r'^[^@]+@[^@]+\.[^@]+$', + }), + isFalse, + ); + expect( + () => + registry.invoke('regex', {'value': 'any', 'pattern': 'invalid['}), + throwsA(isA()), + ); // Invalid regex pattern + }); + + test('length', () { + expect(registry.invoke('length', {'value': 'abc', 'min': 3}), isTrue); + expect(registry.invoke('length', {'value': 'ab', 'min': 3}), isFalse); + expect(registry.invoke('length', {'value': 'abc', 'max': 3}), isTrue); + expect(registry.invoke('length', {'value': 'abcd', 'max': 3}), isFalse); + expect( + registry.invoke('length', { + 'value': [1, 2, 3], + 'min': 2, + 'max': 4, + }), + isTrue, + ); + }); + + test('numeric', () { + expect( + registry.invoke('numeric', {'value': 10, 'min': 5, 'max': 15}), + isTrue, + ); + expect(registry.invoke('numeric', {'value': 4, 'min': 5}), isFalse); + expect(registry.invoke('numeric', {'value': 16, 'max': 15}), isFalse); + expect(registry.invoke('numeric', {'value': 'not a number'}), isFalse); + }); + + test('email', () { + expect(registry.invoke('email', {'value': 'test@example.com'}), isTrue); + expect(registry.invoke('email', {'value': 'invalid'}), isFalse); + }); + + test('formatString', () { + expect(registry.invoke('formatString', {'value': 'Hello'}), 'Hello'); + expect(registry.invoke('formatString', {'value': 123}), '123'); + expect(registry.invoke('formatString', {}), ''); + }); + + test('formatNumber', () { + // Basic formatting + expect( + registry.invoke('formatNumber', {'value': 1234.56}), + '1,234.56', + ); // Default locale might vary but usually has grouping + expect( + registry.invoke('formatNumber', { + 'value': 1234.56, + 'decimalPlaces': 1, + }), + '1,234.6', + ); // Rounding + expect( + registry.invoke('formatNumber', { + 'value': 1234.56, + 'decimalPlaces': 2, + 'useGrouping': false, + }), + '1234.56', + ); // No grouping + }); + + test('formatCurrency', () { + // We can't easily test exact output without forcing locale, but we can + // test it doesn't crash. + expect( + registry.invoke('formatCurrency', { + 'value': 100, + 'currencyCode': 'USD', + }), + contains('100.00'), + ); + }); + + test('formatDate', () { + final date = DateTime(2023, 1, 1); + expect( + registry.invoke('formatDate', { + 'value': date.toIso8601String(), + 'pattern': 'yyyy-MM-dd', + }), + '2023-01-01', + ); + expect( + registry.invoke('formatDate', { + 'value': date.millisecondsSinceEpoch, + 'pattern': 'yyyy', + }), + '2023', + ); + }); + + test('pluralize', () { + expect( + registry.invoke('pluralize', { + 'count': 0, + 'zero': 'no items', + 'one': 'one item', + 'other': 'items', + }), + 'no items', + ); + expect( + registry.invoke('pluralize', { + 'count': 1, + 'zero': 'no items', + 'one': 'one item', + 'other': 'items', + }), + 'one item', + ); + expect( + registry.invoke('pluralize', { + 'count': 5, + 'zero': 'no items', + 'one': 'one item', + 'other': 'items', + }), + 'items', + ); + }); + }); + }); +} diff --git a/packages/genui/test/genui_surface_test.dart b/packages/genui/test/genui_surface_test.dart index 3c8df88c3..6d22d20e9 100644 --- a/packages/genui/test/genui_surface_test.dart +++ b/packages/genui/test/genui_surface_test.dart @@ -8,14 +8,18 @@ import 'package:genui/genui.dart'; import 'package:logging/logging.dart'; void main() { - late A2uiMessageProcessor processor; + late SurfaceController controller; final testCatalog = Catalog([ - CoreCatalogItems.button, - CoreCatalogItems.text, + BasicCatalogItems.button, + BasicCatalogItems.text, ], catalogId: 'test_catalog'); setUp(() { - processor = A2uiMessageProcessor(catalogs: [testCatalog]); + controller = SurfaceController(catalogs: [testCatalog]); + }); + + tearDown(() { + controller.dispose(); }); testWidgets('SurfaceWidget builds a widget from a definition', ( @@ -25,36 +29,26 @@ void main() { final components = [ const Component( id: 'root', - componentProperties: { - 'Button': { - 'child': 'text', - 'action': {'name': 'testAction'}, - }, - }, - ), - const Component( - id: 'text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Hello'}, + type: 'Button', + properties: { + 'child': 'text', + 'action': { + 'event': {'name': 'testAction'}, }, }, ), + const Component(id: 'text', type: 'Text', properties: {'text': 'Hello'}), ]; - processor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), ); - processor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'root', - catalogId: 'test_catalog', - ), + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( - home: GenUiSurface(host: processor, surfaceId: surfaceId), + home: Surface(surfaceContext: controller.contextFor(surfaceId)), ), ); @@ -67,39 +61,30 @@ void main() { final components = [ const Component( id: 'root', - componentProperties: { - 'Button': { - 'child': 'text', - 'action': {'name': 'testAction'}, - }, - }, - ), - const Component( - id: 'text', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Hello'}, + type: 'Button', + properties: { + 'child': 'text', + 'action': { + 'event': {'name': 'testAction'}, }, }, ), + const Component(id: 'text', type: 'Text', properties: {'text': 'Hello'}), ]; - processor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), ); - processor.handleMessage( - const BeginRendering( - surfaceId: surfaceId, - root: 'root', - catalogId: 'test_catalog', - ), + controller.handleMessage( + const CreateSurface(surfaceId: surfaceId, catalogId: 'test_catalog'), ); await tester.pumpWidget( MaterialApp( - home: GenUiSurface(host: processor, surfaceId: surfaceId), + home: Surface(surfaceContext: controller.contextFor(surfaceId)), ), ); - + await tester.pumpAndSettle(); + expect(find.byType(ElevatedButton), findsOneWidget); await tester.tap(find.byType(ElevatedButton)); }); @@ -110,21 +95,17 @@ void main() { final components = [ const Component( id: 'root', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Hello'}, - }, - }, + type: 'Text', + properties: {'text': 'Hello'}, ), ]; - processor.handleMessage( - SurfaceUpdate(surfaceId: surfaceId, components: components), + controller.handleMessage( + UpdateComponents(surfaceId: surfaceId, components: components), ); - // Request a catalogId that doesn't exist in the processor. - processor.handleMessage( - const BeginRendering( + // Request a catalogId that doesn't exist in the controller. + controller.handleMessage( + const CreateSurface( surfaceId: surfaceId, - root: 'root', catalogId: 'non_existent_catalog', ), ); @@ -134,13 +115,16 @@ void main() { await tester.pumpWidget( MaterialApp( - home: GenUiSurface(host: processor, surfaceId: surfaceId), + home: Surface(surfaceContext: controller.contextFor(surfaceId)), ), ); - // Should build an empty container instead of the widget tree. - expect(find.byType(Container), findsOneWidget); - expect(find.byType(Text), findsNothing); + // Should build an FallbackWidget instead of the widget tree. + expect(find.byType(FallbackWidget), findsOneWidget); + expect( + find.textContaining('Catalog with id "non_existent_catalog" not found'), + findsOneWidget, + ); // Should log a severe error. expect( diff --git a/packages/genui/test/image_test.dart b/packages/genui/test/image_test.dart index 6ad248904..fe8b41416 100644 --- a/packages/genui/test/image_test.dart +++ b/packages/genui/test/image_test.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/src/catalog/core_widgets/image.dart'; +import 'package:genui/src/catalog/basic_catalog_widgets/image.dart'; import 'package:genui/src/model/catalog_item.dart'; import 'package:genui/src/model/data_model.dart'; import 'package:genui/src/model/ui_models.dart'; @@ -21,11 +21,10 @@ void main() { builder: (context) => Scaffold( body: image.widgetBuilder( CatalogItemContext( + type: 'Image', data: { - 'url': { - 'literalString': - 'https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png', - }, + 'url': + 'https://storage.googleapis.com/cms-storage-bucket/lockup_flutter_horizontal.c823e53b3a1a7b0d36a9.png', }, id: 'test_image', buildChild: (_, [_]) => const SizedBox(), @@ -33,6 +32,7 @@ void main() { buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -61,9 +61,10 @@ void main() { builder: (context) => Scaffold( body: image.widgetBuilder( CatalogItemContext( + type: 'Image', data: { - 'url': {'literalString': 'https://example.com/avatar.png'}, - 'usageHint': 'avatar', + 'url': 'https://example.com/avatar.png', + 'variant': 'avatar', }, id: 'test_image_avatar', buildChild: (_, [_]) => const SizedBox(), @@ -71,6 +72,7 @@ void main() { buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -82,7 +84,12 @@ void main() { expect(find.byType(CircleAvatar), findsOneWidget); final Finder sizeBoxFinder = find.ancestor( of: find.byType(Image), - matching: find.byType(SizedBox), + matching: find.byWidgetPredicate( + (widget) => + widget is SizedBox && + widget.width == 32.0 && + widget.height == 32.0, + ), ); expect(sizeBoxFinder, findsOneWidget); final SizedBox sizeBox = tester.widget(sizeBoxFinder); @@ -101,9 +108,10 @@ void main() { builder: (context) => Scaffold( body: image.widgetBuilder( CatalogItemContext( + type: 'Image', data: { - 'url': {'literalString': 'https://example.com/header.png'}, - 'usageHint': 'header', + 'url': 'https://example.com/header.png', + 'variant': 'header', }, id: 'test_image_header', buildChild: (_, [_]) => const SizedBox(), @@ -111,6 +119,7 @@ void main() { buildContext: context, dataContext: DataContext(DataModel(), '/'), getComponent: (String componentId) => null, + getCatalogItem: (String type) => null, surfaceId: 'surface1', ), ), @@ -121,12 +130,14 @@ void main() { final Finder sizeBoxFinder = find.ancestor( of: find.byType(Image), - matching: find.byType(SizedBox), + matching: find.byWidgetPredicate( + (widget) => + widget is SizedBox && + widget.width == double.infinity && + widget.height == null, + ), ); expect(sizeBoxFinder, findsOneWidget); - final SizedBox sizeBox = tester.widget(sizeBoxFinder); - expect(sizeBox.width, double.infinity); - expect(sizeBox.height, null); }); }); } diff --git a/packages/genui/test/model/a2ui_message_test.dart b/packages/genui/test/model/a2ui_message_test.dart new file mode 100644 index 000000000..bab54c0a4 --- /dev/null +++ b/packages/genui/test/model/a2ui_message_test.dart @@ -0,0 +1,161 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/catalog/basic_catalog.dart'; +import 'package:genui/src/model/a2ui_message.dart'; +import 'package:genui/src/model/catalog.dart'; +import 'package:genui/src/model/data_model.dart'; +import 'package:genui/src/model/ui_models.dart'; +import 'package:genui/src/primitives/simple_items.dart'; +import 'package:json_schema_builder/src/schema/schema.dart'; + +void main() { + group('A2uiMessage', () { + // ... existing tests ... + test('CreateSurface.fromJson parses correctly', () { + final Map json = { + 'version': 'v0.9', + 'createSurface': { + surfaceIdKey: 's1', + 'catalogId': 'catalog1', + 'theme': {'color': 'blue'}, + 'sendDataModel': true, + }, + }; + + final message = A2uiMessage.fromJson(json); + expect(message, isA()); + final create = message as CreateSurface; + expect(create.surfaceId, 's1'); + expect(create.catalogId, 'catalog1'); + expect(create.theme, {'color': 'blue'}); + expect(create.sendDataModel, isTrue); + }); + + test('UpdateComponents.fromJson parses correctly', () { + final Map json = { + 'version': 'v0.9', + 'updateComponents': { + surfaceIdKey: 's1', + 'components': [ + {'id': 'c1', 'component': 'Text', 'text': 'Hello'}, + ], + }, + }; + + final message = A2uiMessage.fromJson(json); + expect(message, isA()); + final update = message as UpdateComponents; + expect(update.surfaceId, 's1'); + expect(update.components.length, 1); + expect(update.components.first.id, 'c1'); + expect(update.components.first.type, 'Text'); + }); + + test('UpdateDataModel.fromJson parses correctly', () { + final Map json = { + 'version': 'v0.9', + 'updateDataModel': { + surfaceIdKey: 's1', + 'path': '/user/name', + 'value': 'Alice', + }, + }; + + final message = A2uiMessage.fromJson(json); + expect(message, isA()); + final update = message as UpdateDataModel; + expect(update.surfaceId, 's1'); + expect(update.path, DataPath('/user/name')); + expect(update.value, 'Alice'); + }); + + test('DeleteSurface.fromJson parses correctly', () { + final Map json = { + 'version': 'v0.9', + 'deleteSurface': {surfaceIdKey: 's1'}, + }; + + final message = A2uiMessage.fromJson(json); + expect(message, isA()); + final delete = message as DeleteSurface; + expect(delete.surfaceId, 's1'); + }); + + test('CreateSurface.toJson includes version', () { + const message = CreateSurface(surfaceId: 's1', catalogId: 'c1'); + expect(message.toJson(), containsPair('version', 'v0.9')); + }); + + test('UpdateComponents.toJson includes version', () { + const message = UpdateComponents(surfaceId: 's1', components: []); + expect(message.toJson(), containsPair('version', 'v0.9')); + }); + + test('UpdateDataModel.toJson includes version', () { + const message = UpdateDataModel(surfaceId: 's1'); + expect(message.toJson(), containsPair('version', 'v0.9')); + }); + + test('DeleteSurface.toJson includes version', () { + const message = DeleteSurface(surfaceId: 's1'); + expect(message.toJson(), containsPair('version', 'v0.9')); + }); + + test('fromJson throws on unknown message type', () { + final json = {'version': 'v0.9', 'unknown': {}}; + expect( + () => A2uiMessage.fromJson(json), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Unknown A2UI message type'), + ), + ), + ); + }); + + test('fromJson throws on missing or invalid version', () { + final json = { + 'createSurface': {surfaceIdKey: 's1', 'catalogId': 'c1'}, + }; + expect( + () => A2uiMessage.fromJson(json), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('A2UI message must have version "v0.9"'), + ), + ), + ); + }); + + test('a2uiMessageSchema requires version field', () { + final Catalog catalog = BasicCatalogItems.asCatalog(); + final Schema schema = A2uiMessage.a2uiMessageSchema(catalog); + final json = jsonDecode(schema.toJson()) as Map; + + // Structure is combined -> allOf -> [object] + expect(json['allOf'], isA>()); + final allOf = json['allOf'] as List; + expect(allOf, isNotEmpty); + final mainSchema = allOf.first as Map; + + final properties = mainSchema['properties'] as Map; + expect(properties, contains('version')); + + final required = mainSchema['required'] as List; + expect(required, contains('version')); + + final versionSchema = properties['version'] as Map; + // Depending on json_schema_builder version, it might be 'const' or 'enum' + // But we expect it to enforce 'v0.9' + expect(versionSchema, containsPair('const', 'v0.9')); + }); + }); +} diff --git a/packages/genui/test/model/catalog_exception_test.dart b/packages/genui/test/model/catalog_exception_test.dart new file mode 100644 index 000000000..de171cf80 --- /dev/null +++ b/packages/genui/test/model/catalog_exception_test.dart @@ -0,0 +1,98 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/genui.dart'; +// import 'package:genui/src/model/catalog.dart'; // Exceptions should be exported by genui.dart, but if not we might need this. +// Assuming CatalogItemNotFoundException is exported or available. + +void main() { + group('Catalog Exception', () { + testWidgets( + 'buildWidget throws CatalogItemNotFoundException when item is missing', + (tester) async { + final catalog = const Catalog([], catalogId: 'test_catalog'); + + await tester.pumpWidget(Container()); + final BuildContext context = tester.element(find.byType(Container)); + + final itemContext = CatalogItemContext( + data: {}, + id: 'test_id', + type: 'NonExistentWidget', + buildChild: (id, [context]) => const SizedBox(), + dispatchEvent: (event) {}, + buildContext: context, + dataContext: DataContext(DataModel(), '/'), + getComponent: (id) => null, + getCatalogItem: (type) => null, + surfaceId: 'test_surface', + ); + + expect( + () => catalog.buildWidget(itemContext), + throwsA( + isA() + .having((e) => e.widgetType, 'widgetType', 'NonExistentWidget') + .having((e) => e.catalogId, 'catalogId', 'test_catalog') + .having( + (e) => e.toString(), + 'toString', + contains( + 'CatalogItemNotFoundException: Item "NonExistentWidget" ' + 'was not found in catalog "test_catalog"', + ), + ), + ), + ); + }, + ); + + testWidgets( + 'buildWidget throws CatalogItemNotFoundException without catalogId', + (tester) async { + final catalog = const Catalog([]); + + await tester.pumpWidget(Container()); + final BuildContext context = tester.element(find.byType(Container)); + + final itemContext = CatalogItemContext( + data: {}, + id: 'test_id', + type: 'MissingWidget', + buildChild: (id, [context]) => const SizedBox(), + dispatchEvent: (event) {}, + buildContext: context, + dataContext: DataContext(DataModel(), '/'), + getComponent: (id) => null, + getCatalogItem: (type) => null, + surfaceId: 'test_surface', + ); + + expect( + () => catalog.buildWidget(itemContext), + throwsA( + isA() + .having((e) => e.widgetType, 'widgetType', 'MissingWidget') + .having((e) => e.catalogId, 'catalogId', isNull) + .having( + (e) => e.toString(), + 'toString', + contains( + 'CatalogItemNotFoundException: Item "MissingWidget" ' + 'was not found in catalog', + ), + ) + .having( + (e) => e.toString(), + 'toString', + isNot(contains('"null"')), + ), + ), + ); + }, + ); + }); +} diff --git a/packages/genui/test/model/data_model_edge_cases_test.dart b/packages/genui/test/model/data_model_edge_cases_test.dart new file mode 100644 index 000000000..dd2649c41 --- /dev/null +++ b/packages/genui/test/model/data_model_edge_cases_test.dart @@ -0,0 +1,155 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/model/data_model.dart'; + +void main() { + group('DataModel Edge Cases', () { + late DataModel dataModel; + + setUp(() { + dataModel = DataModel(); + }); + + test('Implicit Structure: Creates nested maps by default', () { + dataModel.update(DataPath('/a/b/c'), 1); + final int? value = dataModel.getValue(DataPath('/a/b/c')); + expect(value, 1); + + final Map? mapA = dataModel + .getValue>(DataPath('/a')); + expect(mapA, isA>()); + expect(mapA?['b'], isA>()); + expect((mapA?['b'] as Map)['c'], 1); + }); + + test('Implicit Structure: Creates list if next segment is integer', () { + dataModel.update(DataPath('/list/0/item'), 'first'); + + final List? list = dataModel.getValue>( + DataPath('/list'), + ); + expect(list, isA>()); + expect(list?.length, 1); + + final Object? item0 = list?[0]; + expect(item0, isA>()); + expect((item0 as Map)['item'], 'first'); + }); + + test('Implicit Structure: Creates list of lists', () { + dataModel.update(DataPath('/matrix/0/0'), 1); + + final List? matrix = dataModel.getValue>( + DataPath('/matrix'), + ); + expect(matrix, isA>()); + expect(matrix?[0], isA>()); + expect((matrix?[0] as List)[0], 1); + }); + + test('Type Mismatch: Overwriting primitive with map fails silently ' + 'or clobbers?', () { + // Setup: /a is a String + dataModel.update(DataPath('/a'), 'hello'); + + // Attempt to write /a/b (treating /a as map) + // Implementation check: _updateValue checks "if (current is Map)". + // If current is String, it does nothing? + dataModel.update(DataPath('/a/b'), 'world'); + + // Verify /a is still 'hello' + expect(dataModel.getValue(DataPath('/a')), 'hello'); + // Verify /a/b is unresolvable (null) + expect(dataModel.getValue(DataPath('/a/b')), isNull); + }); + + test('Type Mismatch: Overwriting map with primitive clobbers', () { + // Setup: /a/b = 1 + dataModel.update(DataPath('/a/b'), 1); + + // Overwrite /a with primitive + dataModel.update(DataPath('/a'), 'clobbered'); + + expect(dataModel.getValue(DataPath('/a')), 'clobbered'); + expect(dataModel.getValue(DataPath('/a/b')), isNull); + }); + + test('List Boundaries: Append works (index == length)', () { + dataModel.update(DataPath('/list/0'), 'a'); + dataModel.update(DataPath('/list/1'), 'b'); + + expect(dataModel.getValue>(DataPath('/list')), ['a', 'b']); + }); + + test('List Boundaries: Out of bounds (index > length) is ignored', () { + dataModel.update(DataPath('/list/0'), 'a'); + + // Try to write to index 2 (skipping 1) + dataModel.update(DataPath('/list/2'), 'c'); + + expect(dataModel.getValue>(DataPath('/list')), ['a']); + // Verify length is still 1 + final List? list = dataModel.getValue>( + DataPath('/list'), + ); + expect(list?.length, 1); + }); + + test('Null Handling: Setting map key to null removes it', () { + dataModel.update(DataPath('/map'), {'a': 1, 'b': 2}); + dataModel.update(DataPath('/map/a'), null); + + final Map? map = dataModel + .getValue>(DataPath('/map')); + expect(map?.containsKey('a'), isFalse); + expect(map?['b'], 2); + }); + + test('Null Handling: Setting list index to null sets it to null ' + '(does not remove)', () { + dataModel.update(DataPath('/list/0'), 'a'); + dataModel.update(DataPath('/list/0'), null); + + final List? list = dataModel.getValue>( + DataPath('/list'), + ); + expect(list?.length, 1); + expect(list?[0], isNull); + }); + + test('Subscription: Notified when parent structure changes', () { + // Subscribe to /a/b + dataModel.update(DataPath('/a/b'), 1); + final ValueNotifier notifier = dataModel.subscribe( + DataPath('/a/b'), + ); + + int? lastValue; + notifier.addListener(() => lastValue = notifier.value); + + // Update /a (parent), removing b + dataModel.update(DataPath('/a'), {'c': 3}); + + // Notifier should fire and value should be null + expect(lastValue, isNull); + expect(notifier.value, isNull); + }); + + test('Subscription: Notified when structure created under it', () { + final ValueNotifier notifier = dataModel.subscribe( + DataPath('/a/b'), + ); + String? lastValue; + notifier.addListener(() => lastValue = notifier.value); + + // Create properties + dataModel.update(DataPath('/a/b'), 'created'); + + expect(lastValue, 'created'); + }); + }); +} diff --git a/packages/genui/test/model/data_model_test.dart b/packages/genui/test/model/data_model_test.dart index a7540bb02..e66c23ef2 100644 --- a/packages/genui/test/model/data_model_test.dart +++ b/packages/genui/test/model/data_model_test.dart @@ -100,7 +100,7 @@ void main() { }); test('nested creates a new context', () { - final DataContext nested = rootContext.nested(DataPath('a')); + final DataContext nested = rootContext.nested('a'); expect(nested.path, DataPath('/a')); }); }); @@ -168,95 +168,106 @@ void main() { }); }); - group('subscribeToValue', () { - test('notifies on direct updates', () { - final ValueNotifier notifier = dataModel.subscribeToValue( - DataPath('/a'), - ); - int? value; - notifier.addListener(() => value = notifier.value); - dataModel.update(DataPath('/a'), 1); - expect(value, 1); - }); - - test('does not notify on child updates', () { - final ValueNotifier?> notifier = dataModel - .subscribeToValue>(DataPath('/a')); - var callCount = 0; - notifier.addListener(() => callCount++); - dataModel.update(DataPath('/a/b'), 1); - expect(callCount, 0); - }); - - test('does not notify on parent updates', () { - dataModel.update(DataPath('/a/b'), 1); - final ValueNotifier notifier = dataModel.subscribeToValue( - DataPath('/a/b'), - ); - var callCount = 0; - notifier.addListener(() => callCount++); - dataModel.update(DataPath('/a'), {'b': 2}); - expect(callCount, 0); - }); - }); - group('DataModel Update Parsing', () { - test('parses contents with valueString', () { - dataModel.update(DataPath.root, [ - {'key': 'a', 'valueString': 'hello'}, - ]); + test('parses contents with simple string', () { + dataModel.update(DataPath.root, {'a': 'hello'}); expect(dataModel.getValue(DataPath('/a')), 'hello'); }); - test('parses contents with valueNumber', () { - dataModel.update(DataPath.root, [ - {'key': 'b', 'valueNumber': 123}, - ]); + test('parses contents with simple number', () { + dataModel.update(DataPath.root, {'b': 123}); expect(dataModel.getValue(DataPath('/b')), 123); }); - test('parses contents with valueBoolean', () { - dataModel.update(DataPath.root, [ - {'key': 'c', 'valueBoolean': true}, - ]); + test('parses contents with simple boolean', () { + dataModel.update(DataPath.root, {'c': true}); expect(dataModel.getValue(DataPath('/c')), isTrue); }); - test('parses contents with valueMap', () { - dataModel.update(DataPath.root, [ - { - 'key': 'd', - 'valueMap': [ - {'key': 'd1', 'valueString': 'v1'}, - {'key': 'd2', 'valueNumber': 2}, - ], - }, - ]); + test('parses contents with simple map', () { + dataModel.update(DataPath.root, { + 'd': {'d1': 'v1', 'd2': 2}, + }); expect(dataModel.getValue>(DataPath('/d')), { 'd1': 'v1', 'd2': 2, }); }); - test('is permissive with multiple value types', () { - dataModel.update(DataPath.root, [ - {'key': 'e', 'valueString': 'first', 'valueNumber': 999}, - ]); - expect(dataModel.getValue(DataPath('/e')), 'first'); - }); - - test('handles empty contents array', () { + test('handles empty contents map', () { dataModel.update(DataPath('/a'), {'b': 1}); // Initial data - dataModel.update(DataPath.root, []); - expect(dataModel.data, isEmpty); + dataModel.update(DataPath.root, {}); + // Root update merges into the root model. + // Verify it doesn't crash. }); + }); + }); - test('handles contents with no value field', () { - dataModel.update(DataPath.root, [ - {'key': 'f'}, - ]); - expect(dataModel.getValue(DataPath('/f')), isNull); - }); + group('DataModel External State Binding', () { + late DataModel dataModel; + + setUp(() { + dataModel = DataModel(); + }); + + test('bindExternalState initializes model from source', () { + final source = ValueNotifier(42); + dataModel.bindExternalState(path: DataPath('/external'), source: source); + expect(dataModel.getValue(DataPath('/external')), 42); + }); + + test('bindExternalState updates model when source changes', () { + final source = ValueNotifier(0); + dataModel.bindExternalState(path: DataPath('/external'), source: source); + + source.value = 10; + expect(dataModel.getValue(DataPath('/external')), 10); + }); + + test( + 'bindExternalState updates source when model changes (twoWay=true)', + () { + final source = ValueNotifier(0); + dataModel.bindExternalState( + path: DataPath('/external'), + source: source, + twoWay: true, + ); + + dataModel.update(DataPath('/external'), 99); + expect(source.value, 99); + }, + ); + + test( + '''bindExternalState does NOT update source when model changes (twoWay=false)''', + () { + final source = ValueNotifier(0); + dataModel.bindExternalState( + path: DataPath('/external'), + source: source, + twoWay: false, + ); + + dataModel.update(DataPath('/external'), 99); + expect(source.value, 0); + }, + ); + + test('bindExternalState handles cleanup on dispose', () { + final source = ValueNotifier(0); + dataModel.bindExternalState( + path: DataPath('/external'), + source: source, + twoWay: true, + ); + + dataModel.dispose(); + + // Update data model shouldn't crash but won't update source if disposed? + // Update data model shouldn't crash but won't update source if disposed? + // Verify behavior: if we update source, model shouldn't update. + // But model is disposed. }); }); diff --git a/packages/genui/test/model/function_resolution_test.dart b/packages/genui/test/model/function_resolution_test.dart new file mode 100644 index 000000000..bacf807e3 --- /dev/null +++ b/packages/genui/test/model/function_resolution_test.dart @@ -0,0 +1,70 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/functions/functions.dart'; +import 'package:genui/src/model/data_model.dart'; + +void main() { + group('DataContext Function Resolution', () { + late DataModel dataModel; + late DataContext context; + + setUp(() { + dataModel = DataModel(); + context = DataContext(dataModel, '/'); + // Ensure standard functions are registered (singleton) + FunctionRegistry(); + }); + + test('resolves simple function call', () { + final Map input = { + 'call': 'formatNumber', + 'args': {'value': 1234.56, 'decimalPlaces': 1}, + }; + final Object? result = context.resolve(input); + // Default standard formatNumber uses current locale, might vary, + // but usually '1,234.6' or '1234.6' depending on environment. + // Let's check regex or simpler function first. + expect(result, isA()); + }); + + test('resolves required function returning boolean', () { + final Map input = { + 'call': 'required', + 'args': {'value': 'some value'}, + }; + expect(context.resolve(input), isTrue); + }); + + test('resolves nested function calls', () { + final Map input = { + 'call': 'required', + 'args': { + 'value': { + 'call': 'formatString', + 'args': {'value': ''}, + }, + }, + }; + // formatString('') -> '' + // required('') -> false + expect(context.resolve(input), isFalse); + }); + + test('resolves arguments with expressions', () { + dataModel.update(DataPath('/name'), 'World'); + final Map input = { + 'call': 'formatString', + 'args': {'value': r'Hello ${/name}'}, + }; + expect(context.resolve(input), 'Hello World'); + }); + + test('returns original object if not a function call', () { + final input = {'other': 'value'}; + expect(context.resolve(input), input); + }); + }); +} diff --git a/packages/genui/test/model/ui_definition_test.dart b/packages/genui/test/model/surface_definition_test.dart similarity index 63% rename from packages/genui/test/model/ui_definition_test.dart rename to packages/genui/test/model/surface_definition_test.dart index c7603f23a..392094e9f 100644 --- a/packages/genui/test/model/ui_definition_test.dart +++ b/packages/genui/test/model/surface_definition_test.dart @@ -6,18 +6,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:genui/genui.dart'; void main() { - group('UiDefinition', () { + group('SurfaceDefinition', () { test('toJson() serializes correctly', () { - final definition = UiDefinition( + final definition = SurfaceDefinition( surfaceId: 'testSurface', - rootComponentId: 'root', catalogId: 'test_catalog', components: { 'root': const Component( id: 'root', - componentProperties: { - 'Text': {'text': 'Hello'}, - }, + type: 'Text', + properties: {'text': 'Hello'}, ), }, ); @@ -25,14 +23,8 @@ void main() { final JsonMap json = definition.toJson(); expect(json[surfaceIdKey], 'testSurface'); - expect(json['rootComponentId'], 'root'); expect(json['components'], { - 'root': { - 'id': 'root', - 'component': { - 'Text': {'text': 'Hello'}, - }, - }, + 'root': {'id': 'root', 'component': 'Text', 'text': 'Hello'}, }); }); }); diff --git a/packages/genui/test/model/ui_models_test.dart b/packages/genui/test/model/ui_models_test.dart index 97c7edd6b..ee7d7ab56 100644 --- a/packages/genui/test/model/ui_models_test.dart +++ b/packages/genui/test/model/ui_models_test.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/src/model/tools.dart'; import 'package:genui/src/model/ui_models.dart'; import 'package:genui/src/primitives/simple_items.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; void main() { group('UserActionEvent', () { @@ -23,7 +23,6 @@ void main() { expect(event.name, 'testAction'); expect(event.sourceComponentId, 'testWidget'); expect(event.timestamp, now); - expect(event.isAction, isTrue); expect(event.context, {'key': 'value'}); }); @@ -34,7 +33,6 @@ void main() { 'name': 'testAction', 'sourceComponentId': 'testWidget', 'timestamp': now.toIso8601String(), - 'isAction': true, 'context': {'key': 'value'}, }); @@ -42,7 +40,6 @@ void main() { expect(event.name, 'testAction'); expect(event.sourceComponentId, 'testWidget'); expect(event.timestamp, now); - expect(event.isAction, isTrue); expect(event.context, {'key': 'value'}); }); @@ -62,8 +59,64 @@ void main() { expect(map['name'], 'testAction'); expect(map['sourceComponentId'], 'testWidget'); expect(map['timestamp'], now.toIso8601String()); - expect(map['isAction'], isTrue); expect(map['context'], {'key': 'value'}); }); }); + + group('SurfaceDefinition', () { + test('validate throws exception on mismatch', () { + final component = const Component( + id: 'test', + type: 'Text', + properties: {'text': 'Hello'}, + ); + final uiDef = SurfaceDefinition( + surfaceId: 's1', + components: {'test': component}, + ); + + // Schema invalidating the component (e.g., expecting type "Button") + final schema = S.object( + properties: { + 'components': S.list( + items: S.object( + properties: {'component': S.string(constValue: 'Button')}, + ), + ), + }, + ); + + expect( + () => uiDef.validate(schema), + throwsA(isA()), + ); + }); + + test('validate passes on correct match', () { + final component = const Component( + id: 'test', + type: 'Text', + properties: {'text': 'Hello'}, + ); + final uiDef = SurfaceDefinition( + surfaceId: 's1', + components: {'test': component}, + ); + + final schema = S.object( + properties: { + 'components': S.list( + items: S.object( + properties: { + 'component': S.string(constValue: 'Text'), + 'text': S.string(), + }, + ), + ), + }, + ); + + uiDef.validate(schema); // Should not throw + }); + }); } diff --git a/packages/genui/test/transport/a2ui_parser_transformer_test.dart b/packages/genui/test/transport/a2ui_parser_transformer_test.dart new file mode 100644 index 000000000..993685f07 --- /dev/null +++ b/packages/genui/test/transport/a2ui_parser_transformer_test.dart @@ -0,0 +1,143 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:genui/src/model/a2ui_message.dart'; +import 'package:genui/src/model/generation_events.dart'; +import 'package:genui/src/model/ui_models.dart'; +import 'package:genui/src/transport/a2ui_parser_transformer.dart'; +import 'package:test/test.dart'; + +void main() { + group('A2uiParserTransformer', () { + late StreamController controller; + late Stream stream; + + setUp(() { + controller = StreamController(); + stream = controller.stream.transform(const A2uiParserTransformer()); + }); + + tearDown(() { + controller.close(); + }); + + test('emits pure text chunks as TextEvents', () async { + final StreamQueue queue = StreamQueue(stream); + controller.add('Hello '); + controller.add('World'); + + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', 'Hello '), + ); + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', 'World'), + ); + await queue.cancel(); + }); + + test('extracts Markdown JSON block', () async { + final StreamQueue queue = StreamQueue(stream); + + controller.add('Here is a message:\n'); + controller.add('```json\n'); + controller.add( + '{"version": "v0.9", "createSurface": {"surfaceId": "foo", ' + '"catalogId": "cat"}}\n', + ); + controller.add('```\n'); + controller.add('End of message.'); + + expect( + (await queue.next) as TextEvent, + isA().having( + (e) => e.text, + 'text', + contains('Here is a message:'), + ), + ); + + final msgEvent = (await queue.next) as A2uiMessageEvent; + expect(msgEvent.message, isA()); + expect((msgEvent.message as CreateSurface).surfaceId, 'foo'); + + // The text after might be just the newline or "End of message." + // We accept whatever text comes next, potentially fragmented. + var lastText = (await queue.next) as TextEvent; + while (!lastText.text.contains('End of message.')) { + if (!await queue.hasNext) break; + final GenerationEvent event = await queue.next; + if (event is TextEvent) { + lastText = TextEvent(lastText.text + event.text); + } else { + fail( + 'Expected text event containing "End of message.", found $event', + ); + } + } + expect(lastText.text, contains('End of message.')); + + await queue.cancel(); + }); + + test('extracts Balanced JSON block split across chunks', () async { + final StreamQueue queue = StreamQueue(stream); + + controller.add('Start '); + controller.add('{ "version": "v0.9", "deleteSurface": '); + controller.add('{ "surfaceId": '); // Needs nesting for wrapper + controller.add('"bar" } }'); + controller.add(' End'); + + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', 'Start '), + ); + + final msgEvent = (await queue.next) as A2uiMessageEvent; + expect(msgEvent.message, isA()); + expect((msgEvent.message as DeleteSurface).surfaceId, 'bar'); + + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', ' End'), + ); + + await queue.cancel(); + }); + + test('flushes buffer on done', () async { + final StreamQueue queue = StreamQueue(stream); + + controller.add('Incomplete { json'); + unawaited(controller.close()); + + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', 'Incomplete '), + ); + expect( + (await queue.next) as TextEvent, + isA().having((e) => e.text, 'text', '{ json'), + ); + expect(await queue.hasNext, isFalse); + }); + + test('emits error for invalid A2UI message', () async { + final StreamQueue queue = StreamQueue(stream); + + // Malformed CreateSurface (missing required fields) + controller.add('{"version": "v0.9", "createSurface": {}}'); + + // Should emit error + expect(queue.next, throwsA(isA())); + + await queue.cancel(); + }); + }); +} diff --git a/packages/genui/test/transport/a2ui_transport_adapter_test.dart b/packages/genui/test/transport/a2ui_transport_adapter_test.dart new file mode 100644 index 000000000..1ee0fa483 --- /dev/null +++ b/packages/genui/test/transport/a2ui_transport_adapter_test.dart @@ -0,0 +1,88 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:genui/src/model/a2ui_message.dart'; + +import 'package:genui/src/transport/a2ui_transport_adapter.dart'; +import 'package:test/test.dart'; + +void main() { + group('A2uiTransportAdapter', () { + late A2uiTransportAdapter controller; + + setUp(() { + controller = A2uiTransportAdapter(); + }); + + tearDown(() { + controller.dispose(); + }); + + test('addChunk flows text to textStream', () async { + final Future textFuture = expectLater( + controller.incomingText, + emitsInOrder(['Hello']), + ); + controller.addChunk('Hello'); + await textFuture; + }); + + test('addChunk with message updates state', () async { + // Using JSON block + final json = '''```json +{"version": "v0.9", "createSurface": {"surfaceId": "test_chunk", "catalogId": "test-cat"}} +```'''; + + final Future stateFuture = expectLater( + controller.incomingMessages, + emits( + isA().having((e) => e.surfaceId, 'id', 'test_chunk'), + ), + ); + + controller.addChunk(json); + await stateFuture; + }); + + test('addMessage updates state directly', () async { + final msg = const CreateSurface( + surfaceId: 'direct_msg', + catalogId: 'direct-cat', + ); + + final Future stateFuture = expectLater( + controller.incomingMessages, + emits( + isA().having((e) => e.surfaceId, 'id', 'direct_msg'), + ), + ); + + controller.addMessage(msg); + await stateFuture; + }); + + test('incomingMessages emits parsable JSON messages', () async { + final adapter = A2uiTransportAdapter(); + + final Future expectation = expectLater( + adapter.incomingMessages, + emits( + predicate((m) { + return m is UpdateComponents && + m.components.length == 1 && + m.components.first.id == 'root'; + }), + ), + ); + + adapter.addChunk('''```json +{"version": "v0.9", "updateComponents": {"surfaceId": "test", "components": [{"id": "root", "component": "Text", "properties": {"text": "Hello"}}]}} +```'''); + + await expectation; + }); + }); +} diff --git a/packages/genui/test/ui_tools_test.dart b/packages/genui/test/ui_tools_test.dart deleted file mode 100644 index 2abe9365f..000000000 --- a/packages/genui/test/ui_tools_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; - -void main() { - group('UI Tools', () { - late A2uiMessageProcessor a2uiMessageProcessor; - late Catalog catalog; - - setUp(() { - catalog = CoreCatalogItems.asCatalog(); - a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [catalog]); - }); - - test('SurfaceUpdateTool sends SurfaceUpdate message', () async { - final tool = SurfaceUpdateTool( - handleMessage: a2uiMessageProcessor.handleMessage, - catalog: catalog, - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'components': [ - { - 'id': 'root', - 'component': { - 'Text': { - 'text': {'literalString': 'Hello'}, - }, - }, - }, - ], - }; - - final Future future = expectLater( - a2uiMessageProcessor.surfaceUpdates, - emits( - isA() - .having((e) => e.surfaceId, surfaceIdKey, 'testSurface') - .having( - (e) => e.definition.components.length, - 'components.length', - 1, - ) - .having( - (e) => e.definition.components.values.first.id, - 'components.first.id', - 'root', - ), - ), - ); - - await tool.invoke(args); - a2uiMessageProcessor.handleMessage( - const BeginRendering(surfaceId: 'testSurface', root: 'root'), - ); - - await future; - }); - - test('BeginRenderingTool sends BeginRendering message', () async { - final tool = BeginRenderingTool( - handleMessage: a2uiMessageProcessor.handleMessage, - catalogId: 'test_catalog', - ); - - final Map args = { - surfaceIdKey: 'testSurface', - 'root': 'root', - }; - - // First, add a component to the surface so that the root can be set. - a2uiMessageProcessor.handleMessage( - const SurfaceUpdate( - surfaceId: 'testSurface', - components: [ - Component( - id: 'root', - componentProperties: { - 'Text': { - 'text': {'literalString': 'Hello'}, - }, - }, - ), - ], - ), - ); - - // Use expectLater to wait for the stream to emit the correct event. - final Future future = expectLater( - a2uiMessageProcessor.surfaceUpdates, - emits( - isA() - .having((e) => e.surfaceId, surfaceIdKey, 'testSurface') - .having( - (e) => e.definition.rootComponentId, - 'rootComponentId', - 'root', - ) - .having( - (e) => e.definition.catalogId, - 'catalogId', - 'test_catalog', - ), - ), - ); - - await tool.invoke(args); - - await future; // Wait for the expectation to be met. - }); - }); -} diff --git a/packages/genui/test/utils/json_block_parser_test.dart b/packages/genui/test/utils/json_block_parser_test.dart new file mode 100644 index 000000000..0b75c0522 --- /dev/null +++ b/packages/genui/test/utils/json_block_parser_test.dart @@ -0,0 +1,64 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:genui/src/utils/json_block_parser.dart'; + +void main() { + group('JsonBlockParser', () { + test('parses simple JSON block', () { + const text = 'Here is some JSON:\n```json\n{"foo": "bar"}\n```'; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect(result, equals({'foo': 'bar'})); + }); + + test('parses multi-line JSON block', () { + const text = ''' +Here is some JSON: +```json +{ + "foo": "bar", + "baz": [ + 1, + 2, + 3 + ] +} +``` +'''; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect( + result, + equals({ + 'foo': 'bar', + 'baz': [1, 2, 3], + }), + ); + }); + + test('parses JSON block without language tag', () { + const text = '```\n{"foo": "bar"}\n```'; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect(result, equals({'foo': 'bar'})); + }); + + test('parses raw JSON in text', () { + const text = 'Some text {"foo": "bar"} more text'; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect(result, equals({'foo': 'bar'})); + }); + + test('parses raw JSON with newlines', () { + const text = 'Some text {\n"foo": "bar"\n} more text'; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect(result, equals({'foo': 'bar'})); + }); + + test('parses raw JSON array', () { + const text = 'Some text ["foo", "bar"] more text'; + final Object? result = JsonBlockParser.parseFirstJsonBlock(text); + expect(result, equals(['foo', 'bar'])); + }); + }); +} diff --git a/packages/genui/test/validation_test_utils.dart b/packages/genui/test/validation_test_utils.dart index 5858a5672..415f75d48 100644 --- a/packages/genui/test/validation_test_utils.dart +++ b/packages/genui/test/validation_test_utils.dart @@ -22,7 +22,7 @@ void validateCatalogExamples( ...catalog.items, ...additionalCatalogs.expand((c) => c.items), ]); - final Schema schema = A2uiSchemas.surfaceUpdateSchema(mergedCatalog); + final Schema schema = A2uiSchemas.updateComponentsSchema(mergedCatalog); for (final CatalogItem item in catalog.items) { group('CatalogItem ${item.name}', () { @@ -48,7 +48,7 @@ void validateCatalogExamples( reason: 'Example must have a component with id "root"', ); - final surfaceUpdate = SurfaceUpdate( + final surfaceUpdate = UpdateComponents( surfaceId: 'test-surface', components: components, ); diff --git a/packages/genui_a2ui/DESIGN.md b/packages/genui_a2ui/DESIGN.md index ccf871f53..1ce14b148 100644 --- a/packages/genui_a2ui/DESIGN.md +++ b/packages/genui_a2ui/DESIGN.md @@ -12,55 +12,47 @@ To provide a seamless and robust way for Flutter applications using `genui` to c The architecture of `genui_a2ui` revolves around two main classes: -1. **`A2uiContentGenerator`**: This class implements the `ContentGenerator` interface from `genui`. It serves as the primary bridge between `GenUiConversation` (the application's interface to the generative UI system) and the A2A server. Instead of generating content locally or calling a direct model API, it delegates communication to the `A2uiAgentConnector`. - - Manages the connection lifecycle. - - Exposes a `Stream` (`a2uiMessageStream`) which `A2uiMessageProcessor` consumes to update the UI. - - Exposes a `Stream` (`textResponseStream`) for any text-based agent responses. - - Exposes a `Stream` (`errorStream`) for handling communication errors. - - Implements `sendRequest` to send user messages/actions to the A2A server. - -2. **`A2uiAgentConnector`**: This class encapsulates the low-level details of the A2A protocol, using the `package:a2a` client library. +1. **`A2uiAgentConnector`**: This class encapsulates the low-level details of the A2A protocol, using the `package:a2a` client library. - Handles WebSocket connection management. - Constructs and sends A2A messages (including user input and UI events). - Receives `A2AStreamEvent`s from the server. - Parses `A2ADataPart`s within the events to extract JSON-encoded A2UI messages. - - Converts the JSON A2UI messages into type-safe `A2uiMessage` objects defined in `genui`. + - Exposes streams of `A2uiMessage` and text events that can be piped into a `SurfaceController`. - Manages conversation state like `taskId` and `contextId` as provided by the A2A server. - - Streams the parsed `A2uiMessage` objects to the `A2uiContentGenerator`. ### Data Flow ```mermaid graph TD User --> FlutterApp[Flutter Application] - FlutterApp --> GenUiConversation[GenUiConversation \n genui] - GenUiConversation -- sendRequest --> A2uiContentGenerator - A2uiContentGenerator -- connectAndSend --> A2uiAgentConnector + FlutterApp --> Conversation[Conversation \n genui] + Conversation -- onSend --> A2uiAgentConnector A2uiAgentConnector -- A2AClient --> A2AServer[A2A Server] A2AServer -- A2AStreamEvent --> A2uiAgentConnector - A2uiAgentConnector -- A2uiMessage --> A2uiContentGenerator - A2uiContentGenerator -- a2uiMessageStream --> A2uiMessageProcessor[A2uiMessageProcessor \n genui] - A2uiMessageProcessor --> GenUiSurface[GenUiSurface \n genui] - GenUiSurface --> FlutterApp - A2uiMessageProcessor -- UiEvent --> A2uiContentGenerator - A2uiContentGenerator -- sendEvent --> A2uiAgentConnector + A2uiAgentConnector -- A2uiMessage --> FlutterApp + FlutterApp -- pipes to --> SurfaceController[SurfaceController \n genui] + SurfaceController -- update state --> Surface[Surface \n genui] + Surface --> FlutterApp + Surface -- UiEvent --> SurfaceController + SurfaceController -- client event --> Conversation ``` -1. User input is sent via `GenUiConversation.sendRequest`. -2. `A2uiContentGenerator` delegates to `A2uiAgentConnector` to send the message to the A2A Server. +1. User input is sent via `Conversation.sendRequest`, which triggers the `onSend` callback. +2. The callback delegates to `A2uiAgentConnector` to send the message to the A2A Server. 3. The server streams back `A2AStreamEvent`s containing A2UI messages. 4. `A2uiAgentConnector` parses these into `A2uiMessage` objects. -5. `A2uiContentGenerator` forwards these messages to `A2uiMessageProcessor` via the `a2uiMessageStream`. -6. `A2uiMessageProcessor` updates the state, causing `GenUiSurface` to re-render. -7. User interactions on `GenUiSurface` generate `UiEvent`s, which are sent back to the server via `A2uiContentGenerator` and `A2uiAgentConnector`'s `sendEvent` method. +5. The application (via manual piping) forwards these messages to `SurfaceController`. +6. `SurfaceController` processes the message and updates the surface state. +7. The state change causes `Surface` to re-render. +8. User interactions on `Surface` generate `UiEvent`s, which are captured by `SurfaceController`, passed to `Conversation`, and then sent back to the server via `A2uiAgentConnector` in the `onSend` callback. ### Alternatives Considered - **Embedding A2A logic directly in `genui`**: Rejected to keep the core framework decoupled from specific communication protocols. -- **Re-implementing rendering logic**: Rejected in favor of leveraging `genui`'s established `A2uiMessageProcessor` and `GenUiSurface` for UI rendering and state management. +- **Re-implementing rendering logic**: Rejected in favor of leveraging `genui`'s established `SurfaceController` and `Surface` for UI rendering and state management. The chosen approach of a separate integration package (`genui_a2ui`) provides the best separation of concerns. ## Summary -`genui_a2ui` acts as a specialized `ContentGenerator` for `genui`. It uses `A2uiAgentConnector` to interact with an A2A server, receives A2UI messages, and feeds them into the `A2uiMessageProcessor` to dynamically drive the Flutter UI. This design ensures modularity and reusability. +`genui_a2ui` provides the `A2uiAgentConnector` which acts as a transport bridge between an A2A server and the `genui` framework. By piping its output streams into `SurfaceController`, developers can drive a dynamic Flutter UI from an A2A backend. diff --git a/packages/genui_a2ui/GEMINI.md b/packages/genui_a2ui/GEMINI.md index a78441284..9f5fb75f1 100644 --- a/packages/genui_a2ui/GEMINI.md +++ b/packages/genui_a2ui/GEMINI.md @@ -6,27 +6,21 @@ This document provides context for AI agents making changes to the `genui_a2ui` ## Key Concepts & Responsibilities -- **`ContentGenerator` Interface:** `genui_a2ui` primarily provides an implementation of the `ContentGenerator` interface from the core `genui` package. +- **Content Generator Integration:** `genui_a2ui` provides the `A2uiAgentConnector` which is designed to be used with `genui`'s `SurfaceController`. - **A2A Communication:** All direct communication with the A2A server happens within this package, mainly in `A2uiAgentConnector` using the `package:a2a` client library. - **A2UI Message Parsing:** This package is responsible for taking the raw data from the A2A server and converting it into the structured `A2uiMessage` objects defined in `genui`. - **UI Event Submission:** It also handles sending UI interaction events from `genui` back to the A2A server. ## Core Classes to Understand -1. **`A2uiContentGenerator`** (`lib/src/a2ui_content_generator.dart`): - - The main entry point for `GenUiConversation`. - - Orchestrates the connection and message flow. - - Listens to events from `A2uiAgentConnector` and forwards them on its own streams (`a2uiMessageStream`, `textResponseStream`, `errorStream`). - - Receives `UiEvent`s from `A2uiMessageProcessor` (via a listener setup in `GenUiConversation`) and passes them to `A2uiAgentConnector` to be sent to the server. - -2. **`A2uiAgentConnector`** (`lib/src/a2ui_agent_connector.dart`): +1. **`A2uiAgentConnector`** (`lib/src/a2ui_agent_connector.dart`): - Handles all WebSocket and JSON-RPC communication with the A2A server using `A2AClient`. - Manages connection state, task ID, and context ID. - `connectAndSend()`: Key method to send a `ChatMessage` and process the streamed response. This involves parsing `A2ADataPart` for A2UI messages. - `sendEvent()`: Sends user interaction data back to the server. - `_processA2uiMessages()`: Crucial for converting raw JSON data into `genui.A2uiMessage` objects. -3. **`AgentCard`** (`lib/src/a2ui_agent_connector.dart`): +2. **`AgentCard`** (`lib/src/a2ui_agent_connector.dart`): - Simple data class for agent metadata. ## Typical Modification Areas @@ -34,11 +28,11 @@ This document provides context for AI agents making changes to the `genui_a2ui` - **Protocol Changes:** Updates to how A2UI messages are parsed or how A2A messages are constructed in `A2uiAgentConnector`. - **Error Handling:** Improvements to error detection and reporting in either class. - **Connection Management:** Changes to how the WebSocket connection is handled in `A2uiAgentConnector`. -- **Stream Management:** Modifications to the StreamControllers in `A2uiContentGenerator`. +- **Stream Management:** Modifications to the StreamControllers in `A2uiAgentConnector`. ## Testing -- `test/a2ui_content_generator_test.dart` contains unit tests, primarily mocking the `A2AClient` to test the `A2uiAgentConnector` and `A2uiContentGenerator` logic in isolation. +- `test/a2ui_agent_connector_test.dart` contains unit tests, primarily mocking the `A2AClient` to test the `A2uiAgentConnector` logic in isolation. ## Dependencies @@ -46,3 +40,9 @@ This document provides context for AI agents making changes to the `genui_a2ui` - `a2a`: A2A client library. - `logging`: For logging. - `uuid`: For message IDs. + +## Development + +- While you could run the `tool/run_all_tests_and_fixes.sh` (or `tool/test_and_fix/bin/test_and_fix.dart`, which is the same thing) each time you want to test a change, it is very inefficient. + - Instead, run the tests for the specific package you are working on. For example, `flutter test packages/genui_a2ui/test`. + - When all the tests in the package pass, and you are about to commit the code, run the tool/run_all_tests_and_fixes.sh script to fix any linting and formatting issues, and to verify that all the tests have been run. diff --git a/packages/genui_a2ui/README.md b/packages/genui_a2ui/README.md index 4edfeb378..f8df9960d 100644 --- a/packages/genui_a2ui/README.md +++ b/packages/genui_a2ui/README.md @@ -5,9 +5,9 @@ An integration package for [`genui`](https://pub.dev/packages/genui) and the [A2 ## Features - **A2A Server Connection:** Establishes and manages a WebSocket connection to any server implementing the A2A protocol. -- **A2UI Message Processing:** Receives and parses A2UI messages (like `SurfaceUpdate`, `DataModelUpdate`, `BeginRendering`) from the A2A stream. -- **Dynamic UI Rendering:** Integrates seamlessly with `genui`'s `GenUiSurface` to render UIs based on the received A2UI messages. -- **Content Generator Implementation:** Provides `A2uiContentGenerator`, a specialized `ContentGenerator` for `genui`'s `GenUiConversation` to handle the A2A communication flow. +- **A2UI Message Processing:** Receives and parses A2UI messages (like `UpdateComponents`, `UpdateDataModel`, `CreateSurface`) from the A2A stream. +- **Dynamic UI Rendering:** Integrates seamlessly with `genui`'s `Surface` to render UIs based on the received A2UI messages. +- **Content Generator Integration:** Works with `genui`'s `SurfaceController` by piping messages from the connector to the controller. - **Event Handling:** Captures UI events from `genui` and sends them back to the A2A server as A2A messages. - **Stateful Conversation:** Maintains the conversation context (`taskId`, `contextId`) with the A2A server. @@ -29,11 +29,13 @@ flutter pub add genui genui_a2ui ### Basic Usage -1. **Initialize `A2uiMessageProcessor`:** Set up `A2uiMessageProcessor` with your widget `Catalog`. -2. **Create `A2uiContentGenerator`:** Instantiate `A2uiContentGenerator`, providing the A2A server `Uri`. -3. **Create `GenUiConversation`:** Pass the `A2uiContentGenerator` to the `GenUiConversation`. -4. **Render with `GenUiSurface`:** Use `GenUiSurface` widgets in your UI to display the agent-generated content. -5. **Send Messages:** Use `GenUiConversation.sendRequest` to send user input to the agent. +1. **Initialize `SurfaceController`:** Set up `SurfaceController` with your widget `Catalog`. +2. **Create `A2uiTransportAdapter`:** dedicated adapter to handle message transport. +3. **Create `A2uiAgentConnector`:** Instantiate `A2uiAgentConnector`, providing the A2A server `Uri`. +4. **Create `Conversation`:** Pass the `SurfaceController` and `A2uiTransportAdapter` to the `Conversation`. +5. **Connect Streams:** Pipe the output of `A2uiAgentConnector` into `A2uiTransportAdapter`. +6. **Render with `Surface`:** Use `Surface` widgets in your UI to display the agent-generated content. +7. **Send Messages:** Use `Conversation.sendRequest` to send user input to the agent. ```dart import 'package:flutter/material.dart'; @@ -81,54 +83,80 @@ class ChatScreen extends StatefulWidget { class _ChatScreenState extends State { final TextEditingController _textController = TextEditingController(); - final A2uiMessageProcessor _a2uiMessageProcessor = - A2uiMessageProcessor(catalog: CoreCatalogItems.asCatalog()); - late final A2uiContentGenerator _contentGenerator; - late final GenUiConversation _uiAgent; + late final A2uiAgentConnector _connector; + late final SurfaceController _controller; + late final A2uiTransportAdapter _transport; + late final Conversation _conversation; + late final StreamSubscription _subscription; + late final StreamSubscription _textSubscription; final List _messages = []; @override void initState() { super.initState(); - _contentGenerator = A2uiContentGenerator( - serverUrl: Uri.parse('http://localhost:8080'), // Replace with your A2A server URL + // Initialize the controller with the catalog + _controller = SurfaceController(catalogs: [CoreCatalogItems.asCatalog()]); + + // Create the transport adapter + _transport = A2uiTransportAdapter( + onSend: _sendMessageToAgent, ); - _uiAgent = GenUiConversation( - contentGenerator: _contentGenerator, - a2uiMessageProcessor: _a2uiMessageProcessor, + + // Create the connector + _connector = A2uiAgentConnector( + url: Uri.parse('http://localhost:8080'), // Replace with your A2A server URL ); - // Listen for text responses from the agent - _contentGenerator.textResponseStream.listen((String text) { - setState(() { - _messages.insert(0, AgentMessage.text(text)); - }); - }); + // Create the conversation facade + _conversation = Conversation( + controller: _controller, + transport: _transport, + ); - // Listen for errors - _contentGenerator.errorStream.listen((ContentGeneratorError error) { - print('Error from ContentGenerator: ${error.error}'); - // Optionally show error to the user + // Listen for text responses from the conversation + _conversation.events.listen((event) { + if (event is ConversationContentReceived) { + setState(() { + if (_messages.isEmpty || _messages.first.role != Role.model) { + _messages.insert(0, ChatMessage.model(event.text)); + } else { + // Append to existing message (simplification) + final lastMsg = _messages.first; + // Recreate message with appended text... + } + }); + } }); + + // Pipe connector output to transport + _subscription = _connector.stream.listen(_transport.addMessage); + _textSubscription = _connector.textStream.listen(_transport.addChunk); + } + + Future _sendMessageToAgent(ChatMessage message) async { + await _connector.connectAndSend(message); } @override void dispose() { _textController.dispose(); - _uiAgent.dispose(); - _a2uiMessageProcessor.dispose(); - _contentGenerator.dispose(); + _conversation.dispose(); + _transport.dispose(); + _controller.dispose(); + _connector.dispose(); + _subscription.cancel(); + _textSubscription.cancel(); super.dispose(); } void _handleSubmitted(String text) { if (text.isEmpty) return; _textController.clear(); - final message = UserMessage.text(text); + final message = ChatMessage.user(text); setState(() { _messages.insert(0, message); }); - _uiAgent.sendRequest(message); + _conversation.sendRequest(message); } @override @@ -156,9 +184,10 @@ class _ChatScreenState extends State { // Surface for the main AI-generated UI SizedBox( height: 300, - child: GenUiSurface( - host: _a2uiMessageProcessor, + child: Surface( surfaceId: 'main_surface', + // Use controller as host + host: _controller, )), ], ), @@ -224,7 +253,6 @@ class _ChatScreenState extends State { ## Key Components -- **`A2uiContentGenerator`**: Implements `ContentGenerator`. Manages the connection to the A2A server and processes incoming A2UI messages, updating the `A2uiMessageProcessor`. - **`A2uiAgentConnector`**: Handles the low-level WebSocket communication with the A2A server, including sending messages and parsing stream events. - **`AgentCard`**: A data class holding metadata about the connected AI agent. diff --git a/packages/genui_a2ui/example/.metadata b/packages/genui_a2ui/example/.metadata deleted file mode 100644 index 2ca28ba1d..000000000 --- a/packages/genui_a2ui/example/.metadata +++ /dev/null @@ -1,33 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "6c794842101b5805e74774cce9f1fdb49cbcd13c" - channel: "beta" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - base_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - - platform: ios - create_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - base_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - - platform: macos - create_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - base_revision: 6c794842101b5805e74774cce9f1fdb49cbcd13c - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/genui_a2ui/example/android/.gitignore b/packages/genui_a2ui/example/android/.gitignore deleted file mode 100644 index be3943c96..000000000 --- a/packages/genui_a2ui/example/android/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java -.cxx/ - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks diff --git a/packages/genui_a2ui/example/android/app/build.gradle.kts b/packages/genui_a2ui/example/android/app/build.gradle.kts deleted file mode 100644 index c3d837e92..000000000 --- a/packages/genui_a2ui/example/android/app/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id("dev.flutter.flutter-gradle-plugin") -} - -android { - namespace = "com.example.example" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.example" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") - } - } -} - -flutter { - source = "../.." -} diff --git a/packages/genui_a2ui/example/android/app/src/debug/AndroidManifest.xml b/packages/genui_a2ui/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index a90346878..000000000 --- a/packages/genui_a2ui/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/main/AndroidManifest.xml b/packages/genui_a2ui/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 040f3ea99..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/genui_a2ui/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt deleted file mode 100644 index cf30de4f4..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.example.example - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() diff --git a/packages/genui_a2ui/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/genui_a2ui/example/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index 3827ad38b..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/main/res/drawable/launch_background.xml b/packages/genui_a2ui/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 6e3711468..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/genui_a2ui/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7..000000000 Binary files a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/genui_a2ui/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79b..000000000 Binary files a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d439148..000000000 Binary files a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34..000000000 Binary files a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eeb..000000000 Binary files a/packages/genui_a2ui/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/genui_a2ui/example/android/app/src/main/res/values-night/styles.xml b/packages/genui_a2ui/example/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index deb01c1c9..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/main/res/values/styles.xml b/packages/genui_a2ui/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 45f156a54..000000000 --- a/packages/genui_a2ui/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - diff --git a/packages/genui_a2ui/example/android/app/src/profile/AndroidManifest.xml b/packages/genui_a2ui/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index a90346878..000000000 --- a/packages/genui_a2ui/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/packages/genui_a2ui/example/android/build.gradle.kts b/packages/genui_a2ui/example/android/build.gradle.kts deleted file mode 100644 index dbee657bb..000000000 --- a/packages/genui_a2ui/example/android/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -val newBuildDir: Directory = - rootProject.layout.buildDirectory - .dir("../../build") - .get() -rootProject.layout.buildDirectory.value(newBuildDir) - -subprojects { - val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) - project.layout.buildDirectory.value(newSubprojectBuildDir) -} -subprojects { - project.evaluationDependsOn(":app") -} - -tasks.register("clean") { - delete(rootProject.layout.buildDirectory) -} diff --git a/packages/genui_a2ui/example/android/gradle.properties b/packages/genui_a2ui/example/android/gradle.properties deleted file mode 100644 index fbee1d8cd..000000000 --- a/packages/genui_a2ui/example/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true diff --git a/packages/genui_a2ui/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/genui_a2ui/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 02767eb1c..000000000 --- a/packages/genui_a2ui/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip diff --git a/packages/genui_a2ui/example/android/settings.gradle.kts b/packages/genui_a2ui/example/android/settings.gradle.kts deleted file mode 100644 index cd62cd10e..000000000 --- a/packages/genui_a2ui/example/android/settings.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.0" apply false - id("org.jetbrains.kotlin.android") version "2.2.0" apply false -} - -include(":app") diff --git a/packages/genui_a2ui/example/ios/.gitignore b/packages/genui_a2ui/example/ios/.gitignore deleted file mode 100644 index 7a7f9873a..000000000 --- a/packages/genui_a2ui/example/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/packages/genui_a2ui/example/ios/Flutter/AppFrameworkInfo.plist b/packages/genui_a2ui/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 0d1408009..000000000 --- a/packages/genui_a2ui/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 15.0 - - diff --git a/packages/genui_a2ui/example/ios/Flutter/Debug.xcconfig b/packages/genui_a2ui/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee85..000000000 --- a/packages/genui_a2ui/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/packages/genui_a2ui/example/ios/Flutter/Release.xcconfig b/packages/genui_a2ui/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee85..000000000 --- a/packages/genui_a2ui/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.pbxproj b/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index ed029208c..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/genui_a2ui/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d42e..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/genui_a2ui/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16e..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5e..000000000 --- a/packages/genui_a2ui/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/packages/genui_a2ui/example/ios/Runner/AppDelegate.swift b/packages/genui_a2ui/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 4cb238206..000000000 --- a/packages/genui_a2ui/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada472..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41ec..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773cd8..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae7..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba09..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12a..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2fd..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b7..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/genui_a2ui/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/genui_a2ui/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/ios/Runner/Base.lproj/Main.storyboard b/packages/genui_a2ui/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516f..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/ios/Runner/Info.plist b/packages/genui_a2ui/example/ios/Runner/Info.plist deleted file mode 100644 index 5458fc418..000000000 --- a/packages/genui_a2ui/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Example - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/packages/genui_a2ui/example/ios/RunnerTests/RunnerTests.swift b/packages/genui_a2ui/example/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 7d077cd5a..000000000 --- a/packages/genui_a2ui/example/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/packages/genui_a2ui/example/lib/main.dart b/packages/genui_a2ui/example/lib/main.dart deleted file mode 100644 index df1e9127c..000000000 --- a/packages/genui_a2ui/example/lib/main.dart +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_a2ui/genui_a2ui.dart'; -import 'package:logging/logging.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - configureGenUiLogging(level: Level.ALL); - runApp(const GenUIExampleApp()); -} - -/// The main application widget. -class GenUIExampleApp extends StatelessWidget { - /// Creates a [GenUIExampleApp]. - const GenUIExampleApp({super.key}); - - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'A2UI Example', - theme: ThemeData(primarySwatch: Colors.deepPurple), - home: const ChatScreen(), - ); - } -} - -/// The main chat screen. -class ChatScreen extends StatefulWidget { - /// Creates a [ChatScreen]. - const ChatScreen({super.key}); - - @override - State createState() => _ChatScreenState(); -} - -class _ChatScreenState extends State { - final TextEditingController _textController = TextEditingController(); - final A2uiMessageProcessor _a2uiMessageProcessor = A2uiMessageProcessor( - catalogs: [CoreCatalogItems.asCatalog()], - ); - late final A2uiContentGenerator _contentGenerator; - late final GenUiConversation _genUiConversation; - final List _surfaceIds = ['default']; - int _currentSurfaceIndex = 0; - StreamSubscription? _surfaceSubscription; - - @override - void initState() { - super.initState(); - _contentGenerator = A2uiContentGenerator( - // Replace this with the address of the A2A server (one that supports the - // A2UI extension) that you wish to connect to. - serverUrl: Uri.parse('http://localhost:10002'), - ); - _genUiConversation = GenUiConversation( - contentGenerator: _contentGenerator, - a2uiMessageProcessor: _a2uiMessageProcessor, - ); - // Initialize with existing surfaces - _surfaceIds.addAll( - _a2uiMessageProcessor.surfaces.keys.where( - (id) => !_surfaceIds.contains(id), - ), - ); - - _surfaceSubscription = _a2uiMessageProcessor.surfaceUpdates.listen(( - update, - ) { - if (update is SurfaceAdded) { - genUiLogger.info('Surface added: ${update.surfaceId}'); - if (!_surfaceIds.contains(update.surfaceId)) { - setState(() { - _surfaceIds.add(update.surfaceId); - // Switch to the new surface - _currentSurfaceIndex = _surfaceIds.length - 1; - }); - } - } else if (update is SurfaceUpdated) { - genUiLogger.info('Surface updated: ${update.surfaceId}'); - // The surface will redraw itself, but we call setState here to ensure - // that any other dependent widgets are also updated. - setState(() {}); - } else if (update is SurfaceRemoved) { - genUiLogger.info('Surface removed: ${update.surfaceId}'); - if (_surfaceIds.contains(update.surfaceId)) { - setState(() { - final int removeIndex = _surfaceIds.indexOf(update.surfaceId); - _surfaceIds.removeAt(removeIndex); - if (_surfaceIds.isEmpty) { - _currentSurfaceIndex = 0; - } else { - if (_currentSurfaceIndex >= removeIndex && - _currentSurfaceIndex > 0) { - _currentSurfaceIndex--; - } - if (_currentSurfaceIndex >= _surfaceIds.length) { - _currentSurfaceIndex = _surfaceIds.length - 1; - } - } - }); - } - } - }); - } - - @override - void dispose() { - _textController.dispose(); - _genUiConversation.dispose(); - _surfaceSubscription?.cancel(); - _a2uiMessageProcessor.dispose(); - _contentGenerator.dispose(); - super.dispose(); - } - - void _handleSubmitted(String text) { - _textController.clear(); - _genUiConversation.sendRequest(UserMessage.text(text)); - } - - void _previousSurface() { - if (_currentSurfaceIndex > 0) { - setState(() { - _currentSurfaceIndex--; - }); - } - } - - void _nextSurface() { - if (_currentSurfaceIndex < _surfaceIds.length - 1) { - setState(() { - _currentSurfaceIndex++; - }); - } - } - - @override - Widget build(BuildContext context) { - if (_surfaceIds.isEmpty) { - return Scaffold( - appBar: AppBar(title: const Text('A2UI Example')), - body: const Center(child: Text('No surfaces available.')), - ); - } - final String currentSurfaceId = _surfaceIds[_currentSurfaceIndex]; - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: _previousSurface, - tooltip: 'Previous Surface', - color: _currentSurfaceIndex > 0 - ? null - : Theme.of(context).disabledColor, - ), - title: Text('Surface: $currentSurfaceId'), - actions: [ - IconButton( - icon: const Icon(Icons.arrow_forward), - onPressed: _nextSurface, - tooltip: 'Next Surface', - color: _currentSurfaceIndex < _surfaceIds.length - 1 - ? null - : Theme.of(context).disabledColor, - ), - ], - ), - body: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 200, maxWidth: 300), - child: Column( - children: [ - Expanded( - child: ValueListenableBuilder>( - valueListenable: _genUiConversation.conversation, - builder: (context, messages, child) { - return ListView.builder( - padding: const EdgeInsets.all(8.0), - reverse: true, - itemBuilder: (_, int index) => - _buildMessage(messages.reversed.toList()[index]), - itemCount: messages.length, - ); - }, - ), - ), - const Divider(height: 1.0), - Container( - decoration: BoxDecoration(color: Theme.of(context).cardColor), - child: _buildTextComposer(), - ), - ], - ), - ), - Expanded( - child: SingleChildScrollView( - child: GenUiSurface( - key: ValueKey(currentSurfaceId), - host: _a2uiMessageProcessor, - surfaceId: currentSurfaceId, - ), - ), - ), - ], - ), - ); - } - - Widget _buildMessage(ChatMessage message) { - final isUserMessage = message is UserMessage; - var text = ''; - if (message is UserMessage) { - text = message.text; - } else if (message is AiTextMessage) { - text = message.text; - } - return Container( - margin: const EdgeInsets.symmetric(vertical: 10.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(right: 16.0), - child: CircleAvatar(child: Text(isUserMessage ? 'U' : 'A')), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - isUserMessage ? 'User' : 'Agent', - style: const TextStyle(fontWeight: FontWeight.bold), - ), - Container( - margin: const EdgeInsets.only(top: 5.0), - child: Text(text), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTextComposer() { - return IconTheme( - data: IconThemeData(color: Theme.of(context).colorScheme.secondary), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - Flexible( - child: TextField( - controller: _textController, - onSubmitted: _handleSubmitted, - decoration: const InputDecoration.collapsed( - hintText: 'Send a message', - ), - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 4.0), - child: IconButton( - icon: const Icon(Icons.send), - onPressed: () => _handleSubmitted(_textController.text), - ), - ), - ], - ), - ), - ); - } -} diff --git a/packages/genui_a2ui/example/linux/.gitignore b/packages/genui_a2ui/example/linux/.gitignore deleted file mode 100644 index d3896c984..000000000 --- a/packages/genui_a2ui/example/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/packages/genui_a2ui/example/linux/CMakeLists.txt b/packages/genui_a2ui/example/linux/CMakeLists.txt deleted file mode 100644 index 7a9a314f9..000000000 --- a/packages/genui_a2ui/example/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "example") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.example") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/packages/genui_a2ui/example/linux/flutter/CMakeLists.txt b/packages/genui_a2ui/example/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd01648..000000000 --- a/packages/genui_a2ui/example/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.cc b/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d23..000000000 --- a/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.h b/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47bc..000000000 --- a/packages/genui_a2ui/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/genui_a2ui/example/linux/flutter/generated_plugins.cmake b/packages/genui_a2ui/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87a7..000000000 --- a/packages/genui_a2ui/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/packages/genui_a2ui/example/linux/runner/CMakeLists.txt b/packages/genui_a2ui/example/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc70..000000000 --- a/packages/genui_a2ui/example/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/packages/genui_a2ui/example/linux/runner/main.cc b/packages/genui_a2ui/example/linux/runner/main.cc deleted file mode 100644 index ceea29f8a..000000000 --- a/packages/genui_a2ui/example/linux/runner/main.cc +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/packages/genui_a2ui/example/linux/runner/my_application.cc b/packages/genui_a2ui/example/linux/runner/my_application.cc deleted file mode 100644 index 3ee16b486..000000000 --- a/packages/genui_a2ui/example/linux/runner/my_application.cc +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "example"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "example"); - } - - gtk_window_set_default_size(window, 1280, 720); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/packages/genui_a2ui/example/linux/runner/my_application.h b/packages/genui_a2ui/example/linux/runner/my_application.h deleted file mode 100644 index 7a9d54f07..000000000 --- a/packages/genui_a2ui/example/linux/runner/my_application.h +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/genui_a2ui/example/macos/.gitignore b/packages/genui_a2ui/example/macos/.gitignore deleted file mode 100644 index 746adbb6b..000000000 --- a/packages/genui_a2ui/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/packages/genui_a2ui/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/genui_a2ui/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/packages/genui_a2ui/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/genui_a2ui/example/macos/Flutter/Flutter-Release.xcconfig b/packages/genui_a2ui/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/packages/genui_a2ui/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/genui_a2ui/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/genui_a2ui/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817a5..000000000 --- a/packages/genui_a2ui/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.pbxproj b/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index a548a4676..000000000 --- a/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* example.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.example.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_a2ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_a2ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/genui_a2ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index ac78810cd..000000000 --- a/packages/genui_a2ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/genui_a2ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16e..000000000 --- a/packages/genui_a2ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/genui_a2ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_a2ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_a2ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_a2ui/example/macos/Runner/AppDelegate.swift b/packages/genui_a2ui/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index 43bd41192..000000000 --- a/packages/genui_a2ui/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba5..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226d..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e0..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72c..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfd..000000000 Binary files a/packages/genui_a2ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/packages/genui_a2ui/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/genui_a2ui/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4e..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_a2ui/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/genui_a2ui/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index dda9752f6..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.example - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/packages/genui_a2ui/example/macos/Runner/Configs/Debug.xcconfig b/packages/genui_a2ui/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd946..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/genui_a2ui/example/macos/Runner/Configs/Release.xcconfig b/packages/genui_a2ui/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f4956..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/genui_a2ui/example/macos/Runner/Configs/Warnings.xcconfig b/packages/genui_a2ui/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf478..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/genui_a2ui/example/macos/Runner/DebugProfile.entitlements b/packages/genui_a2ui/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 3ba6c1266..000000000 --- a/packages/genui_a2ui/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.client - - com.apple.security.network.server - - - diff --git a/packages/genui_a2ui/example/macos/Runner/Info.plist b/packages/genui_a2ui/example/macos/Runner/Info.plist deleted file mode 100644 index 47c3c4207..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Info.plist +++ /dev/null @@ -1,37 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - - diff --git a/packages/genui_a2ui/example/macos/Runner/MainFlutterWindow.swift b/packages/genui_a2ui/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 79861d1c4..000000000 --- a/packages/genui_a2ui/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/packages/genui_a2ui/example/macos/Runner/Release.entitlements b/packages/genui_a2ui/example/macos/Runner/Release.entitlements deleted file mode 100644 index ee95ab7e5..000000000 --- a/packages/genui_a2ui/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/packages/genui_a2ui/example/macos/RunnerTests/RunnerTests.swift b/packages/genui_a2ui/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 8b03e329d..000000000 --- a/packages/genui_a2ui/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/packages/genui_a2ui/example/pubspec.yaml b/packages/genui_a2ui/example/pubspec.yaml deleted file mode 100644 index ea8e3f604..000000000 --- a/packages/genui_a2ui/example/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: example -description: "A new Flutter project." -publish_to: "none" -version: 0.1.0 - -environment: - sdk: ">=3.9.2 <4.0.0" - flutter: ">=3.35.7 <4.0.0" - -resolution: workspace - -dependencies: - flutter: - sdk: flutter - genui: ^0.7.0 - genui_a2ui: ^0.7.0 - logging: ^1.3.0 - -dev_dependencies: - flutter_lints: ^6.0.0 - flutter_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/packages/genui_a2ui/example/web/favicon.png b/packages/genui_a2ui/example/web/favicon.png deleted file mode 100644 index 8aaa46ac1..000000000 Binary files a/packages/genui_a2ui/example/web/favicon.png and /dev/null differ diff --git a/packages/genui_a2ui/example/web/icons/Icon-192.png b/packages/genui_a2ui/example/web/icons/Icon-192.png deleted file mode 100644 index b749bfef0..000000000 Binary files a/packages/genui_a2ui/example/web/icons/Icon-192.png and /dev/null differ diff --git a/packages/genui_a2ui/example/web/icons/Icon-512.png b/packages/genui_a2ui/example/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48df..000000000 Binary files a/packages/genui_a2ui/example/web/icons/Icon-512.png and /dev/null differ diff --git a/packages/genui_a2ui/example/web/icons/Icon-maskable-192.png b/packages/genui_a2ui/example/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d76e..000000000 Binary files a/packages/genui_a2ui/example/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/packages/genui_a2ui/example/web/icons/Icon-maskable-512.png b/packages/genui_a2ui/example/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c56691..000000000 Binary files a/packages/genui_a2ui/example/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/packages/genui_a2ui/example/web/index.html b/packages/genui_a2ui/example/web/index.html deleted file mode 100644 index 43a974954..000000000 --- a/packages/genui_a2ui/example/web/index.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - example - - - - - - diff --git a/packages/genui_a2ui/example/web/manifest.json b/packages/genui_a2ui/example/web/manifest.json deleted file mode 100644 index 096edf8fe..000000000 --- a/packages/genui_a2ui/example/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "example", - "short_name": "example", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/packages/genui_a2ui/example/windows/.gitignore b/packages/genui_a2ui/example/windows/.gitignore deleted file mode 100644 index d492d0d98..000000000 --- a/packages/genui_a2ui/example/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/packages/genui_a2ui/example/windows/CMakeLists.txt b/packages/genui_a2ui/example/windows/CMakeLists.txt deleted file mode 100644 index d960948af..000000000 --- a/packages/genui_a2ui/example/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(example LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "example") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/packages/genui_a2ui/example/windows/flutter/CMakeLists.txt b/packages/genui_a2ui/example/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f4899d..000000000 --- a/packages/genui_a2ui/example/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.cc b/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 8b6d4680a..000000000 --- a/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.h b/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85a..000000000 --- a/packages/genui_a2ui/example/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/genui_a2ui/example/windows/flutter/generated_plugins.cmake b/packages/genui_a2ui/example/windows/flutter/generated_plugins.cmake deleted file mode 100644 index b93c4c30c..000000000 --- a/packages/genui_a2ui/example/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/packages/genui_a2ui/example/windows/runner/CMakeLists.txt b/packages/genui_a2ui/example/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c05..000000000 --- a/packages/genui_a2ui/example/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/genui_a2ui/example/windows/runner/Runner.rc b/packages/genui_a2ui/example/windows/runner/Runner.rc deleted file mode 100644 index 289fc7ee6..000000000 --- a/packages/genui_a2ui/example/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "example" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "example" "\0" - VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "example.exe" "\0" - VALUE "ProductName", "example" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/packages/genui_a2ui/example/windows/runner/flutter_window.cpp b/packages/genui_a2ui/example/windows/runner/flutter_window.cpp deleted file mode 100644 index 84d1989ec..000000000 --- a/packages/genui_a2ui/example/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/packages/genui_a2ui/example/windows/runner/flutter_window.h b/packages/genui_a2ui/example/windows/runner/flutter_window.h deleted file mode 100644 index 9ca6118a7..000000000 --- a/packages/genui_a2ui/example/windows/runner/flutter_window.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/genui_a2ui/example/windows/runner/main.cpp b/packages/genui_a2ui/example/windows/runner/main.cpp deleted file mode 100644 index fb9fb5a8b..000000000 --- a/packages/genui_a2ui/example/windows/runner/main.cpp +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"example", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/packages/genui_a2ui/example/windows/runner/resource.h b/packages/genui_a2ui/example/windows/runner/resource.h deleted file mode 100644 index 66a65d1e4..000000000 --- a/packages/genui_a2ui/example/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/packages/genui_a2ui/example/windows/runner/resources/app_icon.ico b/packages/genui_a2ui/example/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20caf..000000000 Binary files a/packages/genui_a2ui/example/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/packages/genui_a2ui/example/windows/runner/runner.exe.manifest b/packages/genui_a2ui/example/windows/runner/runner.exe.manifest deleted file mode 100644 index 153653e8d..000000000 --- a/packages/genui_a2ui/example/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,14 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - diff --git a/packages/genui_a2ui/example/windows/runner/utils.cpp b/packages/genui_a2ui/example/windows/runner/utils.cpp deleted file mode 100644 index 3f2acf58e..000000000 --- a/packages/genui_a2ui/example/windows/runner/utils.cpp +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - unsigned int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/packages/genui_a2ui/example/windows/runner/utils.h b/packages/genui_a2ui/example/windows/runner/utils.h deleted file mode 100644 index c3891dbcb..000000000 --- a/packages/genui_a2ui/example/windows/runner/utils.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/packages/genui_a2ui/example/windows/runner/win32_window.cpp b/packages/genui_a2ui/example/windows/runner/win32_window.cpp deleted file mode 100644 index fffe7f693..000000000 --- a/packages/genui_a2ui/example/windows/runner/win32_window.cpp +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/packages/genui_a2ui/example/windows/runner/win32_window.h b/packages/genui_a2ui/example/windows/runner/win32_window.h deleted file mode 100644 index 9ee3eb04f..000000000 --- a/packages/genui_a2ui/example/windows/runner/win32_window.h +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/genui_a2ui/lib/genui_a2ui.dart b/packages/genui_a2ui/lib/genui_a2ui.dart index 48d2a479d..943bf2f44 100644 --- a/packages/genui_a2ui/lib/genui_a2ui.dart +++ b/packages/genui_a2ui/lib/genui_a2ui.dart @@ -3,4 +3,3 @@ // found in the LICENSE file. export 'src/a2ui_agent_connector.dart'; -export 'src/a2ui_content_generator.dart'; diff --git a/packages/genui_a2ui/lib/src/a2a/client/a2a_client.dart b/packages/genui_a2ui/lib/src/a2a/client/a2a_client.dart index 783f7c641..cabfaeb7b 100644 --- a/packages/genui_a2ui/lib/src/a2a/client/a2a_client.dart +++ b/packages/genui_a2ui/lib/src/a2a/client/a2a_client.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:logging/logging.dart'; +import '../../logging_utils.dart'; import '../core/agent_card.dart'; import '../core/events.dart'; import '../core/list_tasks_params.dart'; @@ -200,7 +201,13 @@ class A2AClient { return stream.transform( StreamTransformer.fromHandlers( handleData: (data, sink) { - _log?.fine('Received event from stream: $data'); + try { + _log?.fine( + () => 'Received event from stream: ${sanitizeLogData(data)}', + ); + } catch (e) { + _log?.warning('Error logging event from stream: $e'); + } if (data.containsKey('error')) { sink.addError( _exceptionFrom(data['error'] as Map), @@ -313,7 +320,13 @@ class A2AClient { 'id': _requestId++, }) .map((data) { - _log?.fine('Received event from stream: $data'); + try { + _log?.fine( + () => 'Received event from stream: ${sanitizeLogData(data)}', + ); + } catch (e) { + _log?.warning('Error logging event from stream: $e'); + } if (data.containsKey('error')) { throw _exceptionFrom(data['error'] as Map); } diff --git a/packages/genui_a2ui/lib/src/a2a/client/sse_parser.dart b/packages/genui_a2ui/lib/src/a2a/client/sse_parser.dart index 89d54b273..19f44770a 100644 --- a/packages/genui_a2ui/lib/src/a2a/client/sse_parser.dart +++ b/packages/genui_a2ui/lib/src/a2a/client/sse_parser.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:logging/logging.dart'; +import '../../logging_utils.dart'; import 'a2a_exception.dart'; /// A parser for Server-Sent Events (SSE). @@ -27,7 +28,10 @@ class SseParser { try { await for (final line in lines) { - log?.finer('Received SSE line: $line'); + final String lineData = line.length < 300 + ? line + : line.substring(0, 300); + log?.finer('Received SSE line: ${line.length} $lineData...'); if (line.startsWith('data:')) { data.add(line.substring(5).trim()); } else if (line.startsWith(':')) { @@ -36,39 +40,62 @@ class SseParser { } else if (line.isEmpty) { // Event boundary if (data.isNotEmpty) { - final String dataString = data.join('\n'); + final Map? result = _parseData(data); data = []; // Clear for next event - if (dataString.isNotEmpty) { - try { - final jsonData = jsonDecode(dataString) as Map; - log?.finer('Parsed JSON: $jsonData'); - if (jsonData.containsKey('result')) { - final Object? result = jsonData['result']; - if (result != null) { - yield result as Map; - } else { - log?.warning('Received a null result in the SSE stream.'); - } - } else if (jsonData.containsKey('error')) { - final error = jsonData['error'] as Map; - throw A2AException.jsonRpc( - code: error['code'] as int, - message: error['message'] as String, - data: error['data'] as Map?, - ); - } - } catch (e) { - throw A2AException.parsing(message: e.toString()); - } + if (result != null) { + yield result; } } } else { log?.warning('Ignoring unexpected SSE line: $line'); } } + + if (data.isNotEmpty) { + log?.finer( + 'End of stream reached with ${data.length} lines of data pending.', + ); + final Map? result = _parseData(data); + if (result != null) { + yield result; + } + } // ignore: avoid_catching_errors } on StateError { throw const A2AException.parsing(message: 'Stream closed unexpectedly.'); } } + + Map? _parseData(List data) { + final String dataString = data.join('\n'); + if (dataString.isNotEmpty) { + try { + final jsonData = jsonDecode(dataString) as Map; + try { + log?.finer(() => 'Parsed JSON: ${sanitizeLogData(jsonData)}'); + } catch (e) { + log?.warning('Error logging parsed JSON: $e'); + } + if (jsonData.containsKey('result')) { + final Object? result = jsonData['result']; + if (result != null) { + return result as Map; + } else { + log?.warning('Received a null result in the SSE stream.'); + } + } else if (jsonData.containsKey('error')) { + final error = jsonData['error'] as Map; + throw A2AException.jsonRpc( + code: error['code'] as int, + message: error['message'] as String, + data: error['data'] as Map?, + ); + } + } catch (e) { + if (e is A2AException) rethrow; + throw A2AException.parsing(message: e.toString()); + } + } + return null; + } } diff --git a/packages/genui_a2ui/lib/src/a2a/client/sse_transport.dart b/packages/genui_a2ui/lib/src/a2a/client/sse_transport.dart index c8265247f..bc64215b6 100644 --- a/packages/genui_a2ui/lib/src/a2a/client/sse_transport.dart +++ b/packages/genui_a2ui/lib/src/a2a/client/sse_transport.dart @@ -11,6 +11,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import '../../logging_utils.dart'; import 'a2a_exception.dart'; import 'http_transport.dart'; import 'sse_parser.dart'; @@ -42,7 +43,15 @@ class SseTransport extends HttpTransport { }) async* { final Uri uri = Uri.parse(url); final String body = jsonEncode(request); - log?.fine('Sending SSE request to $uri with body: $body'); + try { + log?.fine( + () => + 'Sending SSE request to $uri with body: ' + '${jsonEncode(sanitizeLogData(request))}', + ); + } catch (e) { + log?.warning('Error logging SSE request: $e'); + } final Map allHeaders = { 'Content-Type': 'application/json', 'Accept': 'text/event-stream', diff --git a/packages/genui_a2ui/lib/src/a2ui_agent_connector.dart b/packages/genui_a2ui/lib/src/a2ui_agent_connector.dart index 8eaed8f25..1e68e6848 100644 --- a/packages/genui_a2ui/lib/src/a2ui_agent_connector.dart +++ b/packages/genui_a2ui/lib/src/a2ui_agent_connector.dart @@ -11,11 +11,12 @@ import 'package:logging/logging.dart'; import 'package:uuid/uuid.dart'; import 'a2a/a2a.dart'; +import 'logging_utils.dart'; export 'a2a/a2a.dart' show AgentCard; final Uri a2uiExtensionUri = Uri.parse( - 'https://a2ui.org/a2a-extension/a2ui/v0.8', + 'https://a2ui.org/a2a-extension/a2ui/v0.9', ); final Logger _log = genui.genUiLogger; @@ -45,20 +46,26 @@ class A2uiAgentConnector { final Uri url; final _controller = StreamController.broadcast(); + final _textController = StreamController.broadcast(); final _errorController = StreamController.broadcast(); @visibleForTesting late A2AClient client; + + /// The current task ID from the A2A server. @visibleForTesting String? taskId; String? _contextId; + + /// The current context ID from the A2A server. String? get contextId => _contextId; - /// The stream of A2UI protocol lines. - /// - /// This stream emits the JSONL messages from the A2UI protocol. + /// The stream of A2UI messages. Stream get stream => _controller.stream; + /// The stream of text responses. + Stream get textStream => _textController.stream; + /// A stream of errors from the A2A connection. Stream get errorStream => _errorController.stream; @@ -70,55 +77,61 @@ class A2uiAgentConnector { /// Connects to the agent and sends a message. /// + /// The [clientCapabilities] describe the UI capabilities of the client, + /// specifically determining which component catalogs are supported. + /// + /// The [clientDataModel] allows passing the current state of client-side + /// data to the agent, enabling context-aware responses. + /// /// Returns the text response from the agent, if any. Future connectAndSend( genui.ChatMessage chatMessage, { genui.A2UiClientCapabilities? clientCapabilities, + Map? clientDataModel, + genui.CancellationSignal? cancellationSignal, }) async { - final List parts = switch (chatMessage) { - genui.UserMessage(parts: final p) => p, - genui.UserUiInteractionMessage(parts: final p) => p, - _ => [], - }; + cancellationSignal?.addListener(() { + if (taskId != null) { + client.cancelTask(taskId!); + } + }); final message = Message( messageId: const Uuid().v4(), role: Role.user, - parts: parts.map((part) { - switch (part) { - case genui.TextPart(): - return Part.text(text: part.text); - case genui.DataPart(): - return Part.data(data: part.data as Map? ?? {}); - case genui.ImagePart(): - if (part.url != null) { - return Part.file( - file: FileType.uri( - uri: part.url.toString(), - mimeType: part.mimeType, - ), - ); - } else { - String base64Data; - if (part.bytes != null) { - base64Data = base64Encode(part.bytes!); - } else if (part.base64 != null) { - base64Data = part.base64!; - } else { - _log.warning('ImagePart has no data (url, bytes, or base64)'); - return const Part.text(text: '[Empty Image]'); - } - return Part.file( - file: FileType.bytes( - bytes: base64Data, - mimeType: part.mimeType, - ), - ); + parts: chatMessage.parts.map((part) { + if (part is genui.TextPart) { + return Part.text(text: part.text); + } else if (part.isUiInteractionPart) { + final genui.UiInteractionPart uiPart = part.asUiInteractionPart!; + try { + final Object? json = jsonDecode(uiPart.interaction); + if (json is Map) { + return Part.data(data: json); } - default: - _log.warning('Unknown message part type: ${part.runtimeType}'); - return const Part.text(text: '[Unknown Part]'); + return Part.text(text: uiPart.interaction); + } catch (e) { + return Part.text(text: uiPart.interaction); + } + } else if (part.isUiPart) { + final genui.UiPart uiPart = part.asUiPart!; + return Part.data(data: uiPart.definition.toJson()); + } else if (part is genui.DataPart) { + return Part.file( + file: FileType.bytes( + bytes: base64Encode(part.bytes), + mimeType: part.mimeType, + ), + ); + } else if (part is genui.LinkPart) { + return Part.file( + file: FileType.uri( + uri: part.url.toString(), + mimeType: part.mimeType ?? 'application/octet-stream', + ), + ); } + return const Part.text(text: ''); }).toList(), ); @@ -129,19 +142,29 @@ class A2uiAgentConnector { if (contextId != null) { messageToSend = messageToSend.copyWith(contextId: contextId); } + + final metadata = {}; if (clientCapabilities != null) { - messageToSend = messageToSend.copyWith( - metadata: {'a2uiClientCapabilities': clientCapabilities.toJson()}, - ); + metadata['a2uiClientCapabilities'] = clientCapabilities.toJson(); + } + if (clientDataModel != null) { + metadata['a2uiClientDataModel'] = clientDataModel; + } + if (metadata.isNotEmpty) { + messageToSend = messageToSend.copyWith(metadata: metadata); } _log.info('--- OUTGOING REQUEST ---'); - _log.info('URL: ${url.toString()}'); + _log.info('URL: $url'); _log.info('Method: message/stream'); - _log.info( - 'Payload: ' - '${const JsonEncoder.withIndent(' ').convert(messageToSend.toJson())}', - ); + try { + final String payload = const JsonEncoder.withIndent( + ' ', + ).convert(sanitizeLogData(messageToSend.toJson())); + _log.info('Payload: $payload'); + } catch (e) { + _log.warning('Error logging payload: $e'); + } _log.info('----------------------'); final Stream events = client.messageStream(messageToSend); @@ -182,6 +205,10 @@ class A2uiAgentConnector { for (final Part part in message.parts) { if (part is DataPart) { _processA2uiMessages(part.data); + } else if (part is TextPart) { + if (!_textController.isClosed) { + _textController.add(part.text); + } } } } @@ -213,6 +240,10 @@ class A2uiAgentConnector { for (final Part part in message.parts) { if (part is DataPart) { _processA2uiMessages(part.data); + } else if (part is TextPart) { + if (!_textController.isClosed) { + _textController.add(part.text); + } } } } @@ -225,8 +256,12 @@ class A2uiAgentConnector { } } } - } on FormatException catch (e, s) { - _log.severe('Error parsing A2A response: $e', e, s); + } on FormatException catch (exception, stackTrace) { + _log.severe( + 'Error parsing A2A response: $exception', + exception, + stackTrace, + ); } return responseText; } @@ -242,15 +277,19 @@ class A2uiAgentConnector { } final Map clientEvent = { - 'actionName': event['action'], - 'sourceComponentId': event['sourceComponentId'], - 'timestamp': DateTime.now().toIso8601String(), - 'resolvedContext': event['context'], + 'version': 'v0.9', + 'action': { + 'name': event['action'], + 'sourceComponentId': event['sourceComponentId'], + 'timestamp': DateTime.now().toIso8601String(), + 'context': event['context'], + if (event.containsKey('surfaceId')) 'surfaceId': event['surfaceId'], + }, }; _log.finest('Sending client event: $clientEvent'); - final dataPart = Part.data(data: {'a2uiEvent': clientEvent}); + final dataPart = Part.data(data: clientEvent); final message = Message( role: Role.user, parts: [dataPart], @@ -275,19 +314,21 @@ class A2uiAgentConnector { } void _processA2uiMessages(Map data) { - _log.finest( - 'Processing a2ui messages from data part:\n' - '${const JsonEncoder.withIndent(' ').convert(data)}', - ); - if (data.containsKey('surfaceUpdate') || - data.containsKey('dataModelUpdate') || - data.containsKey('beginRendering') || + var prettyJson = '(Error sanitizing log data)'; + try { + prettyJson = const JsonEncoder.withIndent( + ' ', + ).convert(sanitizeLogData(data)); + _log.finest('Processing a2ui messages from data part:\n$prettyJson'); + } catch (e) { + _log.warning('Error logging a2ui messages: $e'); + } + if (data.containsKey('updateComponents') || + data.containsKey('updateDataModel') || + data.containsKey('createSurface') || data.containsKey('deleteSurface')) { if (!_controller.isClosed) { - _log.finest( - 'Adding message to stream: ' - '${const JsonEncoder.withIndent(' ').convert(data)}', - ); + _log.finest('Adding message to stream: $prettyJson'); _controller.add(genui.A2uiMessage.fromJson(data)); } } else { @@ -303,6 +344,9 @@ class A2uiAgentConnector { if (!_controller.isClosed) { _controller.close(); } + if (!_textController.isClosed) { + _textController.close(); + } if (!_errorController.isClosed) { _errorController.close(); } diff --git a/packages/genui_a2ui/lib/src/a2ui_content_generator.dart b/packages/genui_a2ui/lib/src/a2ui_content_generator.dart deleted file mode 100644 index fddbaa3de..000000000 --- a/packages/genui_a2ui/lib/src/a2ui_content_generator.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:genui/genui.dart'; - -import 'a2ui_agent_connector.dart'; - -/// A content generator that connects to an A2UI server. -class A2uiContentGenerator implements ContentGenerator { - /// Creates an [A2uiContentGenerator] instance. - /// - /// If optional `connector` is not supplied, then one will be created with the - /// given `serverUrl`. - A2uiContentGenerator({required Uri serverUrl, A2uiAgentConnector? connector}) - : connector = connector ?? A2uiAgentConnector(url: serverUrl) { - this.connector.errorStream.listen((Object error) { - _errorResponseController.add( - ContentGeneratorError(error, StackTrace.current), - ); - }); - } - - final A2uiAgentConnector connector; - final _textResponseController = StreamController.broadcast(); - final _errorResponseController = - StreamController.broadcast(); - final _isProcessing = ValueNotifier(false); - - @override - Stream get a2uiMessageStream => connector.stream; - - @override - Stream get textResponseStream => _textResponseController.stream; - - @override - Stream get errorStream => - _errorResponseController.stream; - - @override - ValueListenable get isProcessing => _isProcessing; - - @override - void dispose() { - _textResponseController.close(); - connector.dispose(); - _isProcessing.dispose(); - } - - @override - Future sendRequest( - ChatMessage message, { - Iterable? history, - A2UiClientCapabilities? clientCapabilities, - }) async { - _isProcessing.value = true; - try { - if (history != null && history.isNotEmpty) { - genUiLogger.warning( - 'A2uiContentGenerator is stateful and ignores history.', - ); - } - final String? responseText = await connector.connectAndSend( - message, - clientCapabilities: clientCapabilities, - ); - if (responseText != null && responseText.isNotEmpty) { - _textResponseController.add(responseText); - } - } finally { - _isProcessing.value = false; - } - } -} diff --git a/packages/genui_a2ui/lib/src/logging_utils.dart b/packages/genui_a2ui/lib/src/logging_utils.dart new file mode 100644 index 000000000..73ce9ad96 --- /dev/null +++ b/packages/genui_a2ui/lib/src/logging_utils.dart @@ -0,0 +1,26 @@ +// Copyright 2025 The Flutter Authors. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Sanitizes data for logging purposes. +/// +/// This function recursively traverses the given [data] and replaces the value +/// of any key named "bytes" with the string `"<binary bytes>"`. This is +/// useful for preventing large binary data from cluttering log output. +Object? sanitizeLogData(Object? data) { + if (data is Map) { + final Map sanitized = {}; + for (final MapEntry entry in data.entries) { + final key = entry.key.toString(); + if (key == 'bytes') { + sanitized[key] = ''; + } else { + sanitized[key] = sanitizeLogData(entry.value); + } + } + return sanitized; + } else if (data is List) { + return data.map(sanitizeLogData).toList(); + } + return data; +} diff --git a/packages/genui_a2ui/server_to_client.json b/packages/genui_a2ui/server_to_client.json deleted file mode 100644 index 3cebea8cb..000000000 --- a/packages/genui_a2ui/server_to_client.json +++ /dev/null @@ -1,773 +0,0 @@ -{ - "title": "A2UI Message Schema", - "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", - "type": "object", - "properties": { - "beginRendering": { - "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." - }, - "root": { - "type": "string", - "description": "The ID of the root component to render." - }, - "styles": { - "type": "object", - "description": "Styling information for the UI.", - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - } - } - }, - "required": ["root", "surfaceId"] - }, - "surfaceUpdate": { - "type": "object", - "description": "Updates a surface with a new set of components.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." - }, - "components": { - "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, - "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." - }, - "component": { - "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Text'). The value is an object containing the properties for that specific component.", - "properties": { - "Text": { - "type": "object", - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "fit": { - "type": "string", - "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", - "enum": [ - "contain", - "cover", - "fill", - "none", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "properties": { - "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["name"] - }, - "Video": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": ["center", "end", "start", "stretch"] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } - }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "properties": { - "tabItems": { - "type": "array", - "description": "An array of objects, where each object defines a tab with a title and a child component.", - "items": { - "type": "object", - "properties": { - "title": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "child": { - "type": "string" - } - }, - "required": ["title", "child"] - } - } - }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "properties": { - "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } - } - }, - "Modal": { - "type": "object", - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "properties": { - "name": { - "type": "string" - }, - "context": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "properties": { - "path": { - "type": "string" - }, - "literalString": { - "type": "string" - }, - "literalNumber": { - "type": "number" - }, - "literalBoolean": { - "type": "boolean" - } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] - }, - "CheckBox": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "properties": { - "literalBoolean": { - "type": "boolean" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["label", "value"] - }, - "TextField": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] - }, - "DateTimeInput": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - }, - "outputFormat": { - "type": "string", - "description": "The desired format for the output string after a date or time is selected." - } - }, - "required": ["value"] - }, - "MultipleChoice": { - "type": "object", - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "properties": { - "literalArray": { - "type": "array", - "items": { - "type": "string" - } - }, - "path": { - "type": "string" - } - } - }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } - }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." - } - }, - "required": ["selections", "options"] - }, - "Slider": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "properties": { - "literalNumber": { - "type": "number" - }, - "path": { - "type": "string" - } - } - }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." - }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." - } - }, - "required": ["value"] - } - } - } - }, - "required": ["id", "component"] - } - } - }, - "required": ["surfaceId", "components"] - }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." - }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." - }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { - "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string", - "description": "The key for this data entry." - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string" - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - } - }, - "required": ["key"] - } - } - }, - "required": ["key"] - } - } - }, - "required": ["contents", "surfaceId"] - }, - "deleteSurface": { - "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." - } - }, - "required": ["surfaceId"] - } - } -} diff --git a/packages/genui_a2ui/test/a2ui_agent_connector_test.dart b/packages/genui_a2ui/test/a2ui_agent_connector_test.dart index d68cc2aa9..74cee1de7 100644 --- a/packages/genui_a2ui/test/a2ui_agent_connector_test.dart +++ b/packages/genui_a2ui/test/a2ui_agent_connector_test.dart @@ -56,7 +56,7 @@ void main() { fakeClient.messageStreamHandler = (_) => const Stream.empty(); await connector.connectAndSend( - genui.UserMessage.text('Hi'), + genui.ChatMessage.user('Hi'), clientCapabilities: capabilities, ); @@ -64,7 +64,9 @@ void main() { final a2a.Message sentMessage = fakeClient.lastMessageStreamParams!; expect(sentMessage.metadata, isNotNull); expect(sentMessage.metadata!['a2uiClientCapabilities'], { - 'supportedCatalogIds': ['cat1', 'cat2'], + 'v0.9': { + 'supportedCatalogIds': ['cat1', 'cat2'], + }, }); }); @@ -81,14 +83,14 @@ void main() { parts: [ a2a.Part.data( data: { - 'surfaceUpdate': { + 'version': 'v0.9', + 'updateComponents': { 'surfaceId': 's1', 'components': [ { 'id': 'c1', - 'component': { - 'Column': {'children': []}, - }, + 'component': 'Column', + 'children': [], }, ], }, @@ -106,10 +108,10 @@ void main() { final messages = []; connector.stream.listen(messages.add); - final userMessage = genui.UserMessage([ - const genui.TextPart('Hi'), - const genui.TextPart('There'), - ]); + final userMessage = genui.ChatMessage.user( + '', + parts: [const genui.TextPart('Hi'), const genui.TextPart('There')], + ); final String? responseText = await connector.connectAndSend(userMessage); expect(responseText, 'Hello'); @@ -122,7 +124,7 @@ void main() { expect(connector.contextId, 'context1'); expect(fakeClient.messageStreamCalled, 1); expect(messages.length, 1); - expect(messages.first, isA()); + expect(messages.first, isA()); }); test('connectAndSend sends multiple text parts', () async { @@ -137,10 +139,10 @@ void main() { fakeClient.messageStreamHandler = (_) => Stream.fromIterable(responses); await connector.connectAndSend( - genui.UserMessage([ - const genui.TextPart('Hello'), - const genui.TextPart('World'), - ]), + genui.ChatMessage.user( + '', + parts: [const genui.TextPart('Hello'), const genui.TextPart('World')], + ), ); expect(fakeClient.messageStreamCalled, 1); @@ -171,10 +173,12 @@ void main() { expect(sentMessage.referenceTaskIds, ['task1']); expect(sentMessage.contextId, 'context1'); final dataPart = sentMessage.parts.first as a2a.DataPart; - final a2uiEvent = dataPart.data['a2uiEvent'] as Map; - expect(a2uiEvent['actionName'], 'testAction'); - expect(a2uiEvent['sourceComponentId'], 'c1'); - expect(a2uiEvent['resolvedContext'], {'key': 'value'}); + final Map eventData = dataPart.data; + expect(eventData['version'], 'v0.9'); + final action = eventData['action'] as Map; + expect(action['name'], 'testAction'); + expect(action['sourceComponentId'], 'c1'); + expect(action['context'], {'key': 'value'}); }); test('sendEvent does nothing if taskId is null', () async { diff --git a/packages/genui_a2ui/test/a2ui_content_generator_test.dart b/packages/genui_a2ui/test/a2ui_content_generator_test.dart deleted file mode 100644 index 324b9afb2..000000000 --- a/packages/genui_a2ui/test/a2ui_content_generator_test.dart +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_a2ui/genui_a2ui.dart'; -import 'package:genui_a2ui/src/a2a/a2a.dart' as a2a; - -import 'fakes.dart'; - -void main() { - group('A2uiContentGenerator', () { - late A2uiContentGenerator contentGenerator; - late FakeA2uiAgentConnector fakeConnector; - late FakeA2AClient fakeA2AClient; - - setUp(() { - fakeA2AClient = FakeA2AClient(); - fakeA2AClient.agentCard = const a2a.AgentCard( - name: 'Test Agent', - description: 'A test agent', - version: '1.0.0', - protocolVersion: '0.1.0', - url: 'http://localhost:8080', - capabilities: a2a.AgentCapabilities(), - defaultInputModes: ['text/plain'], - defaultOutputModes: ['text/plain'], - skills: [], - ); - final Uri fakeUrl = Uri.parse('http://fake.url'); - fakeConnector = FakeA2uiAgentConnector(url: fakeUrl) - ..client = fakeA2AClient; - - contentGenerator = A2uiContentGenerator( - serverUrl: fakeUrl, - connector: fakeConnector, - ); - }); - - tearDown(() { - contentGenerator.dispose(); - fakeConnector.dispose(); - }); - - test('sendRequest updates isProcessing', () async { - final userMessage = UserMessage([const TextPart('Hello')]); - - expect(contentGenerator.isProcessing.value, isFalse); - final Future future = contentGenerator.sendRequest( - userMessage, - clientCapabilities: const A2UiClientCapabilities( - supportedCatalogIds: ['test_catalog'], - ), - ); - expect(contentGenerator.isProcessing.value, isTrue); - - await future; - - expect(contentGenerator.isProcessing.value, isFalse); - expect(fakeConnector.lastConnectAndSendChatMessage, userMessage); - }); - - test('sendRequest passes clientCapabilities to connector', () async { - final userMessage = UserMessage([const TextPart('Test')]); - const capabilities = A2UiClientCapabilities( - supportedCatalogIds: ['test_catalog'], - ); - - await contentGenerator.sendRequest( - userMessage, - clientCapabilities: capabilities, - ); - - expect(fakeConnector.lastClientCapabilities, capabilities); - }); - - test('sendRequest adds response to textResponseStream', () async { - final userMessage = UserMessage([const TextPart('Test')]); - final completer = Completer(); - contentGenerator.textResponseStream.listen(completer.complete); - - await contentGenerator.sendRequest( - userMessage, - clientCapabilities: const A2UiClientCapabilities( - supportedCatalogIds: ['test_catalog'], - ), - ); - - expect(await completer.future, 'Fake AI Response'); - }); - - test('errorStream forwards errors from connector', () async { - final completer = Completer(); - contentGenerator.errorStream.listen(completer.complete); - - final testError = Exception('Test Error'); - fakeConnector.addError(testError); - - final ContentGeneratorError capturedError = await completer.future; - expect(capturedError.error, testError); - }); - - test('a2uiMessageStream forwards messages from connector', () async { - final completer = Completer(); - contentGenerator.a2uiMessageStream.listen(completer.complete); - - final testMessage = A2uiMessage.fromJson({ - 'surfaceUpdate': { - 'surfaceId': 's1', - 'components': [ - { - 'id': 'c1', - 'component': { - 'Column': {'children': []}, - }, - }, - ], - }, - }); - fakeConnector.addMessage(testMessage); - - final A2uiMessage capturedMessage = await completer.future; - expect(capturedMessage, testMessage); - }); - }); -} diff --git a/packages/genui_a2ui/test/fakes.dart b/packages/genui_a2ui/test/fakes.dart index 3af55138d..724030c07 100644 --- a/packages/genui_a2ui/test/fakes.dart +++ b/packages/genui_a2ui/test/fakes.dart @@ -135,6 +135,9 @@ class FakeA2uiAgentConnector implements A2uiAgentConnector { @override late a2a.A2AClient client; + @override + Stream get textStream => const Stream.empty(); + genui.ChatMessage? lastConnectAndSendChatMessage; genui.A2UiClientCapabilities? lastClientCapabilities; @@ -142,6 +145,8 @@ class FakeA2uiAgentConnector implements A2uiAgentConnector { Future connectAndSend( genui.ChatMessage chatMessage, { genui.A2UiClientCapabilities? clientCapabilities, + Map? clientDataModel, + genui.CancellationSignal? cancellationSignal, }) async { lastConnectAndSendChatMessage = chatMessage; lastClientCapabilities = clientCapabilities; diff --git a/packages/genui_dartantic/CHANGELOG.md b/packages/genui_dartantic/CHANGELOG.md deleted file mode 100644 index 479d3325a..000000000 --- a/packages/genui_dartantic/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# `genui_dartantic` Changelog - -## 0.7.1 (in progress) - -## 0.7.0 - -- **Internal**: Enable stricter dynamic-related analysis (#652). - -## 0.6.1 - -- Updated `pubspec.yaml` to use the latest version of `dartantic_ai` (2.2.0) -- Re-introduced package to monorepo with `DartanticContentGenerator` (#583, #624). - -## 0.6.0 - -- Initial published release. diff --git a/packages/genui_dartantic/LICENSE b/packages/genui_dartantic/LICENSE deleted file mode 100644 index 650b89564..000000000 --- a/packages/genui_dartantic/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright 2025 The Flutter Authors. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/genui_dartantic/README.md b/packages/genui_dartantic/README.md deleted file mode 100644 index cdfac6c8e..000000000 --- a/packages/genui_dartantic/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# genui_dartantic - -This package provides the integration between `genui` and the Dartantic AI -package. It allows you to use multiple AI providers (OpenAI, Anthropic, Google, -Mistral, Cohere, Ollama) to generate dynamic user interfaces in your Flutter -applications. - -## Features - -- **DartanticContentGenerator:** An implementation of `ContentGenerator` that - uses the dartantic_ai package to connect to various AI providers. -- **Multi-Provider Support:** Use any provider supported by dartantic_ai - including OpenAI, Anthropic, Google, Mistral, Cohere, and Ollama. -- **DartanticContentConverter:** Converts between GenUI `ChatMessage` types and - dartantic_ai `ChatMessage` types. -- **Schema Adaptation:** Converts schemas from `json_schema_builder` to the - `json_schema` format used by dartantic_ai. -- **Additional Tools:** Supports adding custom `AiTool`s to extend the AI's - capabilities via the `additionalTools` parameter. -- **Error Handling:** Exposes an `errorStream` to listen for and handle any - errors during content generation. - -## Getting Started - -To use this package, you will need to configure API keys for your chosen -provider (see API Keys section below). - -Then, you can create an instance of `DartanticContentGenerator` and pass it to -your `GenUiConversation`: - -```dart -import 'package:dartantic_ai/dartantic_ai.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_dartantic/genui_dartantic.dart'; - -final catalog = CoreCatalogItems.asCatalog(); -final genUiManager = GenUiManager(catalog: catalog); - -// Example of a custom tool -final myCustomTool = DynamicAiTool>( - name: 'my_custom_action', - description: 'Performs a custom action.', - parameters: S.object(properties: { - 'detail': S.string(), - }), - invokeFunction: (args) async { - print('Custom action called with: $args'); - return {'status': 'ok'}; - }, -); - -final contentGenerator = DartanticContentGenerator( - provider: Providers.google, // or Providers.openai, Providers.anthropic, etc. - catalog: catalog, - systemInstruction: 'You are a helpful assistant.', - additionalTools: [myCustomTool], -); - -final genUiConversation = GenUiConversation( - genUiManager: genUiManager, - contentGenerator: contentGenerator, - ... -); -``` - -## Supported Providers - -The following AI providers are supported through dartantic_ai: - -- **Google (Gemini):** `GoogleProvider` -- **OpenAI:** `OpenAIProvider` -- **Anthropic (Claude):** `AnthropicProvider` -- **Mistral:** `MistralProvider` -- **Cohere:** `CohereProvider` -- **Ollama:** `OllamaProvider` - -## API Keys - -API keys can be configured in dartantic_ai via environment variables: -- `GEMINI_API_KEY` for Google/Gemini -- `OPENAI_API_KEY` for OpenAI -- `ANTHROPIC_API_KEY` for Anthropic -- etc. - -## Configuration - -You can control which actions the AI is allowed to perform using -`GenUiConfiguration`: - -```dart -final contentGenerator = DartanticContentGenerator( - provider: Providers.google, - catalog: catalog, - configuration: const GenUiConfiguration( - actions: ActionsConfig( - allowCreate: true, // Allow creating new UI surfaces - allowUpdate: true, // Allow updating existing surfaces - allowDelete: false, // Disallow deleting surfaces - ), - ), -); -``` - -## Notes - -- **Stateless Design:** The `DartanticContentGenerator` is stateless and does - not maintain internal conversation history. It uses the `history` parameter - passed to `sendRequest` by `GenUiConversation`, converting GenUI messages to - dartantic format via `DartanticContentConverter`. -- **Image Handling:** Currently, `ImagePart`s provided with only a `url` - (without `bytes` or `base64` data) will be sent to the model as a text - description of the URL, as the image data is not automatically fetched by the - converter. -- **Structured Output:** Uses dartantic_ai's built-in support for structured - output with JSON schemas, which works with tool calling across all providers. diff --git a/packages/genui_dartantic/example/.gitignore b/packages/genui_dartantic/example/.gitignore deleted file mode 100644 index 3820a95c6..000000000 --- a/packages/genui_dartantic/example/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/packages/genui_dartantic/example/.metadata b/packages/genui_dartantic/example/.metadata deleted file mode 100644 index 4c152b331..000000000 --- a/packages/genui_dartantic/example/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "66dd93f9a27ffe2a9bfc8297506ce066ff51265f" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f - base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f - - platform: macos - create_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f - base_revision: 66dd93f9a27ffe2a9bfc8297506ce066ff51265f - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/genui_dartantic/example/README.md b/packages/genui_dartantic/example/README.md deleted file mode 100644 index 1bf7a95a5..000000000 --- a/packages/genui_dartantic/example/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# GenUI Tic Tac Toe - -A demonstration of the `genui` framework, showcasing how to build an AI-powered -interactive game using Flutter and Large Language Models (LLMs). But unlike -typical text-based LLM interactions, this example focuses on a **game-centric -UI** where the AI manipulates a graphical board. - - - -## Features - -- **AI-Powered Gameplay**: Play Tic Tac Toe against Google Gemini, OpenAI - GPT-4, or Anthropic Claude. -- **GenUI Components**: The game board is a custom Flutter widget - (`TicTacToeBoard`) exposed to the AI via a `genui` catalog. -- **Dynamic Interaction**: - - **Auto-Start**: The game begins automatically. - - **Thinking State**: Displays randomized status messages (e.g., - "Strategizing...", "Calculating...") along with a "jumping dots" - animation while the AI thinks. - - **Input Safety**: The user's input is visually and functionally disabled - while waiting for the AI, preventing race conditions. - - **Game State Management**: The AI tracks the game state and updates the - board via tool calls. -- **Simplified Configuration**: API keys are managed via environment variables - for easy setup. - -## Setup - -This example requires an API key for at least one of the supported providers -(Google, OpenAI, Anthropic). - -### 1. Get an API Key - -- **Google Gemini**: [Get an API Key](https://aistudio.google.com/app/apikey) -- **OpenAI**: [Get an API Key](https://platform.openai.com/api-keys) -- **Anthropic**: [Get an API Key](https://console.anthropic.com/settings/keys) - -### 2. Run the App - -Pass your API key using the `--dart-define` flag when running the app. - -**For Google Gemini:** -```bash -flutter run --dart-define=GEMINI_API_KEY=your_api_key_here -``` - -**For OpenAI:** -```bash -flutter run --dart-define=OPENAI_API_KEY=your_api_key_here -``` - -**For Anthropic:** -```bash -flutter run --dart-define=ANTHROPIC_API_KEY=your_api_key_here -``` - -## How It Works - -1. **Catalog Definition**: The `TicTacToeBoard` is defined in a `Catalog`, - giving the AI knowledge of its schema (a list of 9 strings representing the - board cells). -2. **Tool Use**: The AI doesn't just "talk"; it uses the `showBoard` tool to - render the game state. -3. **Authentication**: The app uses `dartantic` to abstract the different AI - providers. The `ProviderSelectionPage` automatically picks the provider - based on the environment variables you supplied. diff --git a/packages/genui_dartantic/example/analysis_options.yaml b/packages/genui_dartantic/example/analysis_options.yaml deleted file mode 100644 index 6a65e2191..000000000 --- a/packages/genui_dartantic/example/analysis_options.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -include: package:flutter_lints/flutter.yaml diff --git a/packages/genui_dartantic/example/lib/main.dart b/packages/genui_dartantic/example/lib/main.dart deleted file mode 100644 index 25ca70fdc..000000000 --- a/packages/genui_dartantic/example/lib/main.dart +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; - -import 'src/provider_selection_page.dart'; - -void main() { - WidgetsFlutterBinding.ensureInitialized(); - - // Configure logging - Logger.root.level = Level.INFO; - Logger.root.onRecord.listen( - (record) => debugPrint( - '[${record.level.name}] ${record.loggerName}: ${record.message}', - ), - ); - - runApp(const TicTacToeApp()); -} - -class TicTacToeApp extends StatelessWidget { - const TicTacToeApp({super.key}); - - @override - Widget build(BuildContext context) => MaterialApp( - title: 'GenUI Tic Tac Toe', - debugShowCheckedModeBanner: false, - home: const ProviderSelectionPage(), - ); -} diff --git a/packages/genui_dartantic/example/lib/src/catalog.dart b/packages/genui_dartantic/example/lib/src/catalog.dart deleted file mode 100644 index b698a23b8..000000000 --- a/packages/genui_dartantic/example/lib/src/catalog.dart +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:genui/genui.dart'; -import 'package:json_schema_builder/json_schema_builder.dart'; - -import 'tic_tac_toe_board.dart'; - -final Catalog ticTacToeCatalog = Catalog([ - CatalogItem( - name: 'TicTacToeBoard', - dataSchema: S.object( - properties: { - 'cells': S.list( - description: - 'A list of 9 strings representing the board. Use "X" for user, "O" for AI, and empty string for free cells.', - items: S.string(), - minItems: 9, - maxItems: 9, - ), - }, - required: ['cells'], - ), - widgetBuilder: (context) { - final data = context.data as JsonMap; - final cells = (data['cells'] as List).cast(); - return TicTacToeBoard( - cells: cells, - onCellTap: (index) { - context.dispatchEvent( - UserActionEvent( - name: 'cellTap', - sourceComponentId: 'TicTacToeBoard', - context: {'cellIndex': index}, - ), - ); - }, - ); - }, - exampleData: [], - ), -], catalogId: 'a2ui.org:standard_catalog_0_8_0'); diff --git a/packages/genui_dartantic/example/lib/src/game_page.dart b/packages/genui_dartantic/example/lib/src/game_page.dart deleted file mode 100644 index 92872c22a..000000000 --- a/packages/genui_dartantic/example/lib/src/game_page.dart +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:genui/genui.dart'; - -import 'catalog.dart'; -import 'jumping_dots.dart'; -import 'thinking_verbs.dart'; - -class GamePage extends StatefulWidget { - const GamePage({ - required this.generator, - required this.providerName, - super.key, - }); - - final ContentGenerator generator; - final String providerName; - - @override - State createState() => _GamePageState(); -} - -class _GamePageState extends State { - late final A2uiMessageProcessor _genUiManager; - late final GenUiConversation _conversation; - String? _latestSurfaceId; - String? _statusMessage; - String _thinkingVerb = 'Thinking'; - final TextEditingController _textController = TextEditingController(); - - void _updateThinkingVerb() { - if (_conversation.isProcessing.value) { - setState( - () => _thinkingVerb = - thinkingVerbs[Random().nextInt(thinkingVerbs.length)], - ); - } - } - - @override - void initState() { - super.initState(); - - _genUiManager = A2uiMessageProcessor(catalogs: [ticTacToeCatalog]); - _conversation = GenUiConversation( - contentGenerator: widget.generator, - a2uiMessageProcessor: _genUiManager, - onSurfaceAdded: _handleSurfaceAdded, - onTextResponse: _onTextResponse, - onError: (error) { - debugPrint('Error: ${error.error}'); - // NOTE: the lack of output doesn't impact the experience - // if (!mounted) return; - // ScaffoldMessenger.of( - // context, - // ).showSnackBar(SnackBar(content: Text('Error: ${error.error}'))); - }, - ); - _conversation.isProcessing.addListener(_updateThinkingVerb); - - // Auto-start the game - WidgetsBinding.instance.addPostFrameCallback( - (_) => unawaited( - _conversation.sendRequest( - UserMessage([TextPart('Let\'s play Tic Tac Toe')]), - ), - ), - ); - } - - void _handleSurfaceAdded(SurfaceAdded surface) { - if (!mounted) return; - setState(() => _latestSurfaceId = surface.surfaceId); - } - - void _onTextResponse(String text) { - if (!mounted) return; - setState(() => _statusMessage = text); - } - - @override - void dispose() { - _conversation.isProcessing.removeListener(_updateThinkingVerb); - _conversation.dispose(); - _textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - title: Text('Tic Tac Toe with ${widget.providerName}'), - actions: [ - IconButton( - icon: const Icon(Icons.refresh), - onPressed: () { - setState(() => _statusMessage = 'Restarting game...'); - unawaited( - _conversation.sendRequest( - UserMessage([TextPart('Start a new game')]), - ), - ); - }, - tooltip: 'Restart Game', - ), - ], - ), - body: _latestSurfaceId == null - ? const Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CircularProgressIndicator(), - SizedBox(height: 16), - Text('Starting game...'), - ], - ), - ) - : Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: ValueListenableBuilder( - valueListenable: _conversation.isProcessing, - builder: (context, isProcessing, child) => Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: Text( - isProcessing ? _thinkingVerb : (_statusMessage ?? ''), - style: Theme.of(context).textTheme.titleMedium, - textAlign: TextAlign.center, - ), - ), - if (isProcessing) ...[ - const SizedBox(width: 4), - Padding( - padding: const EdgeInsets.only(bottom: 6), - child: JumpingDots( - color: - Theme.of( - context, - ).textTheme.titleMedium?.color ?? - Colors.black, - radius: 3, - ), - ), - ], - ], - ), - ), - ), - Expanded( - child: Center( - child: Padding( - padding: const EdgeInsets.all(16), - child: ValueListenableBuilder( - valueListenable: _conversation.isProcessing, - builder: (context, isProcessing, child) => AbsorbPointer( - absorbing: isProcessing, - child: Opacity( - opacity: isProcessing ? 0.5 : 1.0, - child: child, - ), - ), - child: GenUiSurface( - host: _genUiManager, - surfaceId: _latestSurfaceId!, - defaultBuilder: (context) => - const Center(child: CircularProgressIndicator()), - ), - ), - ), - ), - ), - ], - ), - ); -} diff --git a/packages/genui_dartantic/example/lib/src/jumping_dots.dart b/packages/genui_dartantic/example/lib/src/jumping_dots.dart deleted file mode 100644 index 4aebfc3f3..000000000 --- a/packages/genui_dartantic/example/lib/src/jumping_dots.dart +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -class JumpingDots extends StatefulWidget { - const JumpingDots({ - super.key, - this.numberOfDots = 3, - this.color = Colors.black, - this.radius = 2.0, - this.animationDuration = const Duration(milliseconds: 200), - }); - - final int numberOfDots; - final Color color; - final double radius; - final Duration animationDuration; - - @override - State createState() => _JumpingDotsState(); -} - -class _JumpingDotsState extends State - with TickerProviderStateMixin { - late final List _controllers; - late final List> _animations; - - @override - void initState() { - super.initState(); - _controllers = List.generate( - widget.numberOfDots, - (index) => - AnimationController(vsync: this, duration: widget.animationDuration), - ); - - _animations = _controllers.map((controller) { - return Tween( - begin: 0, - end: -6.0, - ).animate(CurvedAnimation(parent: controller, curve: Curves.easeInOut)); - }).toList(); - - _startAnimations(); - } - - Future _startAnimations() async { - for (final controller in _controllers) { - // Stagger the animations - if (!mounted) return; - controller.forward().then((_) { - if (mounted) { - controller.reverse(); - } - }); - await Future.delayed( - const Duration(milliseconds: 100), - ); // Stagger delay - } - await Future.delayed( - const Duration(milliseconds: 1000), - ); // Delay between loops - if (mounted) _startAnimations(); // Loop - } - - @override - void dispose() { - for (final controller in _controllers) { - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.end, // Align dots to bottom of text - children: List.generate(_controllers.length, (index) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 1), - child: AnimatedBuilder( - animation: _animations[index], - builder: (context, child) { - return Transform.translate( - offset: Offset(0, _animations[index].value), - child: CircleAvatar( - radius: widget.radius, - backgroundColor: widget.color, - ), - ); - }, - ), - ); - }), - ); - } -} diff --git a/packages/genui_dartantic/example/lib/src/provider_selection_page.dart b/packages/genui_dartantic/example/lib/src/provider_selection_page.dart deleted file mode 100644 index 60c268d75..000000000 --- a/packages/genui_dartantic/example/lib/src/provider_selection_page.dart +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; -import 'package:flutter/material.dart'; -import 'package:genui_dartantic/genui_dartantic.dart'; - -import 'catalog.dart'; -import 'game_page.dart'; - -enum AiProviderType { - google, - openai, - anthropic; - - String get displayName => switch (this) { - AiProviderType.google => 'Google', - AiProviderType.openai => 'OpenAI', - AiProviderType.anthropic => 'Anthropic', - }; - - String get modelName => switch (this) { - AiProviderType.google => 'gemini-2.5-flash', - AiProviderType.openai => 'gpt-5-mini', - AiProviderType.anthropic => 'claude-haiku-4-5', - }; -} - -class ProviderSelectionPage extends StatefulWidget { - const ProviderSelectionPage({super.key}); - - @override - State createState() => _ProviderSelectionPageState(); -} - -class _ProviderSelectionPageState extends State { - AiProviderType _selectedProvider = AiProviderType.google; - - // API key from dart-define - static const _geminiApiKey = String.fromEnvironment('GEMINI_API_KEY'); - static const _openaiApiKey = String.fromEnvironment('OPENAI_API_KEY'); - static const _anthropicApiKey = String.fromEnvironment('ANTHROPIC_API_KEY'); - - void _startGame() { - final dartantic.Provider provider = switch (_selectedProvider) { - AiProviderType.google => dartantic.GoogleProvider(apiKey: _geminiApiKey), - AiProviderType.openai => dartantic.OpenAIResponsesProvider( - apiKey: _openaiApiKey, - ), - AiProviderType.anthropic => dartantic.AnthropicProvider( - apiKey: _anthropicApiKey, - ), - }; - - final generator = DartanticContentGenerator( - provider: provider, - modelName: _selectedProvider.modelName, - catalog: ticTacToeCatalog, - systemInstruction: _systemInstruction, - ); - - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - GamePage(generator: generator, providerName: provider.displayName), - ), - ); - } - - static const _systemInstruction = ''' -You are a Tic Tac Toe master. The user plays "X" and you play "O". - - -- Keep move commentary to 1-2 sentences. -- Do not rephrase the game state or user's move. -- Do not include JSON representations or "A user interface is shown..." text. - - - -- You MUST call both tools to display the board—never use ASCII art or text representations. -- Parallelize these two calls: - 1. "updateSurface" — create a "TicTacToeBoard" component (ID: "board") with a "cells" array of 9 strings. - 2. "beginRendering" — set the surface root to that ID. -- If you do not call these tools, the user cannot see the board. - - - -- If the user wins, you lose. If you win, the user loses. Full board with no winner is a draw. - -'''; - - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar(title: const Text('New Tic Tac Toe Game')), - body: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DropdownButtonFormField( - initialValue: _selectedProvider, - decoration: const InputDecoration(labelText: 'AI Provider'), - items: AiProviderType.values - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type.displayName), - ), - ) - .toList(), - onChanged: (value) { - if (value != null) setState(() => _selectedProvider = value); - }, - ), - const SizedBox(height: 32), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _startGame, - child: const Text('Start Game'), - ), - ), - ], - ), - ), - ), - ), - ); -} diff --git a/packages/genui_dartantic/example/lib/src/thinking_verbs.dart b/packages/genui_dartantic/example/lib/src/thinking_verbs.dart deleted file mode 100644 index 6df300ef2..000000000 --- a/packages/genui_dartantic/example/lib/src/thinking_verbs.dart +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A list of 100 verbs ending in "ing" to be used as status messages. -const List thinkingVerbs = [ - 'Analyzing', - 'Anticipating', - 'Appraising', - 'Arbitrating', - 'Assembling', - 'Assessing', - 'Bargaining', - 'Building', - 'Calculating', - 'Calibrating', - 'Clouding', - 'Communicating', - 'Computing', - 'Concealing', - 'Concluding', - 'Constructing', - 'Contriving', - 'Covering', - 'Creating', - 'Deciding', - 'Deducing', - 'Defending', - 'Deliberating', - 'Deriving', - 'Designing', - 'Determining', - 'Developing', - 'Devising', - 'Disclosing', - 'Displaying', - 'Drafting', - 'Estimating', - 'Evaluating', - 'Evolving', - 'Exchanging', - 'Exhibiting', - 'Exposing', - 'Fabricating', - 'Fixing', - 'Formulating', - 'Framing', - 'Gauging', - 'Generating', - 'Grading', - 'Guarding', - 'Haggling', - 'Hiding', - 'Indicating', - 'Inferring', - 'Inventing', - 'Judging', - 'Maintaining', - 'Manufacturing', - 'Measuring', - 'Mediating', - 'Mending', - 'Moving', - 'Negotiating', - 'Obscuring', - 'Optimizing', - 'Originating', - 'Planning', - 'Plotting', - 'Predicting', - 'Preserving', - 'Processing', - 'Producing', - 'Protecting', - 'Ranking', - 'Rating', - 'Reasoning', - 'Regulating', - 'Repairing', - 'Resolving', - 'Revealing', - 'Saving', - 'Scheming', - 'Scoring', - 'Securing', - 'Shielding', - 'Shifting', - 'Showing', - 'Signaling', - 'Simulating', - 'Solving', - 'Strategizing', - 'Supporting', - 'Sustaining', - 'Switching', - 'Synthesizing', - 'Trading', - 'Transferring', - 'Transmitting', - 'Tuning', - 'Unmasking', - 'Upholding', - 'Valuing', - 'Veiling', - 'Working', -]; diff --git a/packages/genui_dartantic/example/lib/src/tic_tac_toe_board.dart b/packages/genui_dartantic/example/lib/src/tic_tac_toe_board.dart deleted file mode 100644 index de6bcb3cc..000000000 --- a/packages/genui_dartantic/example/lib/src/tic_tac_toe_board.dart +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; - -class TicTacToeBoard extends StatelessWidget { - const TicTacToeBoard({required this.cells, this.onCellTap, super.key}); - - /// A list of 9 strings representing the board state. - /// Each string should be "X", "O", or "" (empty). - final List cells; - - /// Callback when a cell is tapped. Returns the index (0-8). - final ValueChanged? onCellTap; - - @override - Widget build(BuildContext context) { - if (cells.length != 9) { - return Center(child: Text('Invalid board state: ${cells.length} cells')); - } - - return AspectRatio( - aspectRatio: 1, - child: GridView.builder( - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemCount: 9, - itemBuilder: (context, index) { - final value = cells[index]; - return InkWell( - onTap: onCellTap != null && value.isEmpty - ? () => onCellTap!(index) - : null, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.black), - color: Colors.white, - ), - child: Center( - child: Text( - value, - style: Theme.of(context).textTheme.displayLarge?.copyWith( - fontWeight: FontWeight.bold, - color: value == 'X' ? Colors.blue : Colors.red, - ), - ), - ), - ), - ); - }, - ), - ); - } -} diff --git a/packages/genui_dartantic/example/macos/.gitignore b/packages/genui_dartantic/example/macos/.gitignore deleted file mode 100644 index 746adbb6b..000000000 --- a/packages/genui_dartantic/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/packages/genui_dartantic/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/genui_dartantic/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/packages/genui_dartantic/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/genui_dartantic/example/macos/Flutter/Flutter-Release.xcconfig b/packages/genui_dartantic/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b60..000000000 --- a/packages/genui_dartantic/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/genui_dartantic/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/genui_dartantic/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817a5..000000000 --- a/packages/genui_dartantic/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.pbxproj b/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 518e210f9..000000000 --- a/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* tic_tac_toe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "tic_tac_toe.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* tic_tac_toe.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* tic_tac_toe.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.ticTacToe.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tic_tac_toe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tic_tac_toe"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.ticTacToe.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tic_tac_toe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tic_tac_toe"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.ticTacToe.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tic_tac_toe.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/tic_tac_toe"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_dartantic/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_dartantic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/genui_dartantic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index bf2249b7e..000000000 --- a/packages/genui_dartantic/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_dartantic/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/genui_dartantic/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16e..000000000 --- a/packages/genui_dartantic/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/genui_dartantic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/genui_dartantic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/packages/genui_dartantic/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/packages/genui_dartantic/example/macos/Runner/AppDelegate.swift b/packages/genui_dartantic/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index 43bd41192..000000000 --- a/packages/genui_dartantic/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba5..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226d..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e0..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72c..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfd..000000000 Binary files a/packages/genui_dartantic/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/packages/genui_dartantic/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/genui_dartantic/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4e..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/genui_dartantic/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/genui_dartantic/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 44b9acfdf..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = tic_tac_toe - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.ticTacToe - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/packages/genui_dartantic/example/macos/Runner/Configs/Debug.xcconfig b/packages/genui_dartantic/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd946..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/genui_dartantic/example/macos/Runner/Configs/Release.xcconfig b/packages/genui_dartantic/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f4956..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/genui_dartantic/example/macos/Runner/Configs/Warnings.xcconfig b/packages/genui_dartantic/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf478..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/genui_dartantic/example/macos/Runner/DebugProfile.entitlements b/packages/genui_dartantic/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 08c3ab17c..000000000 --- a/packages/genui_dartantic/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - - diff --git a/packages/genui_dartantic/example/macos/Runner/Info.plist b/packages/genui_dartantic/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/packages/genui_dartantic/example/macos/Runner/MainFlutterWindow.swift b/packages/genui_dartantic/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 79861d1c4..000000000 --- a/packages/genui_dartantic/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/packages/genui_dartantic/example/macos/Runner/Release.entitlements b/packages/genui_dartantic/example/macos/Runner/Release.entitlements deleted file mode 100644 index ee95ab7e5..000000000 --- a/packages/genui_dartantic/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/packages/genui_dartantic/example/macos/RunnerTests/RunnerTests.swift b/packages/genui_dartantic/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 8b03e329d..000000000 --- a/packages/genui_dartantic/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/packages/genui_dartantic/example/pubspec.yaml b/packages/genui_dartantic/example/pubspec.yaml deleted file mode 100644 index 5c4f11501..000000000 --- a/packages/genui_dartantic/example/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: tic_tac_toe -description: A Tic Tac Toe example using GenUI and Dartantic. -publish_to: 'none' -version: 0.1.0 - -resolution: workspace - -environment: - sdk: ^3.10.0 - flutter: ">=3.35.7 <4.0.0" - -dependencies: - flutter: - sdk: flutter - genui: ^0.7.0 - genui_dartantic: ^0.7.0 - dartantic_ai: ^3.1.0 - json_schema: ^5.2.0 - json_schema_builder: ^0.1.3 - provider: ^6.0.0 - logging: ^1.3.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -flutter: - uses-material-design: true diff --git a/packages/genui_dartantic/example/readme/genui-tic-tac-toe.mov b/packages/genui_dartantic/example/readme/genui-tic-tac-toe.mov deleted file mode 100644 index 3ee6a3ca4..000000000 Binary files a/packages/genui_dartantic/example/readme/genui-tic-tac-toe.mov and /dev/null differ diff --git a/packages/genui_dartantic/lib/genui_dartantic.dart b/packages/genui_dartantic/lib/genui_dartantic.dart deleted file mode 100644 index 1aa70a32a..000000000 --- a/packages/genui_dartantic/lib/genui_dartantic.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// Integration package for GenUI and Dartantic AI. -/// -/// This library provides a `DartanticContentGenerator` that implements -/// the GenUI `ContentGenerator` interface using the dartantic_ai package. -/// It supports multiple AI providers including OpenAI, Anthropic, Google, -/// Mistral, Cohere, and Ollama. -/// -/// Example usage: -/// ```dart -/// import 'package:dartantic_ai/dartantic_ai.dart'; -/// import 'package:genui/genui.dart'; -/// import 'package:genui_dartantic/genui_dartantic.dart'; -/// -/// final catalog = CoreCatalogItems.asCatalog(); -/// final manager = GenUiManager(catalog: catalog); -/// -/// final contentGenerator = DartanticContentGenerator( -/// provider: Providers.google, -/// catalog: catalog, -/// systemInstruction: 'You are a helpful assistant...', -/// ); -/// -/// final conversation = GenUiConversation( -/// contentGenerator: contentGenerator, -/// genUiManager: manager, -/// ); -/// ``` -library; - -export 'src/dartantic_content_converter.dart'; -export 'src/dartantic_content_generator.dart'; -export 'src/dartantic_schema_adapter.dart'; diff --git a/packages/genui_dartantic/lib/src/dartantic_content_converter.dart b/packages/genui_dartantic/lib/src/dartantic_content_converter.dart deleted file mode 100644 index 0827b4349..000000000 --- a/packages/genui_dartantic/lib/src/dartantic_content_converter.dart +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// Copyright 2025 The Flutter Authors. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:convert'; - -import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; -import 'package:genui/genui.dart' as genui; - -/// An exception thrown by this package. -class ContentConverterException implements Exception { - /// Creates a [ContentConverterException] with the given [message]. - ContentConverterException(this.message); - - /// The message associated with the exception. - final String message; - - @override - String toString() => '$ContentConverterException: $message'; -} - -/// A class to convert between GenUI `ChatMessage` types and text/data formats -/// suitable for dartantic_ai. -/// -/// Since dartantic_ai's Chat class manages conversation history automatically -/// and accepts simple string prompts, this converter primarily extracts text -/// content from GenUI messages. -class DartanticContentConverter { - /// Converts a GenUI [genui.ChatMessage] into a prompt string plus dartantic - /// parts so we can send full multimodal content (text, data, tools). - ({String prompt, List parts}) toPromptAndParts( - genui.ChatMessage message, - ) { - return switch (message) { - genui.UserMessage() => ( - prompt: _extractText(message.parts), - parts: _toPartsWithoutText(message.parts), - ), - genui.UserUiInteractionMessage() => ( - prompt: _extractText(message.parts), - parts: _toPartsWithoutText(message.parts), - ), - genui.AiTextMessage() => ( - prompt: _extractText(message.parts), - parts: _toPartsWithoutText(message.parts), - ), - genui.AiUiMessage() => ( - prompt: _extractText(message.parts), - parts: _toPartsWithoutText(message.parts), - ), - genui.ToolResponseMessage() => ( - prompt: _extractToolResponseText(message.results), - parts: _toolResultPartsToDiParts(message.results), - ), - genui.InternalMessage() => ( - prompt: message.text, - parts: const [], - ), - }; - } - - /// Converts GenUI chat history to a list of dartantic - /// [dartantic.ChatMessage]. - /// - /// Maps GenUI message types to dartantic roles: - /// - [genui.UserMessage], [genui.UserUiInteractionMessage] -> - /// [dartantic.ChatMessage.user] - /// - [genui.AiTextMessage], [genui.AiUiMessage] -> - /// [dartantic.ChatMessage.model] - /// - [genui.ToolResponseMessage] -> [dartantic.ChatMessage.user] with tool - /// results - /// - [genui.InternalMessage] -> [dartantic.ChatMessage.system] - /// - /// If [systemInstruction] is provided, it is added as the first message using - /// [dartantic.ChatMessage.system]. - List toHistory( - Iterable? history, { - String? systemInstruction, - }) { - final result = []; - - // Add system instruction first if provided - if (systemInstruction != null) { - result.add(dartantic.ChatMessage.system(systemInstruction)); - } - - // Convert each GenUI message to dartantic format - if (history != null) { - for (final genui.ChatMessage message in history) { - switch (message) { - case genui.UserMessage(): - result.add( - dartantic.ChatMessage( - role: dartantic.ChatMessageRole.user, - parts: _toParts(message.parts), - ), - ); - case genui.UserUiInteractionMessage(): - result.add( - dartantic.ChatMessage( - role: dartantic.ChatMessageRole.user, - parts: _toParts(message.parts), - ), - ); - case genui.AiTextMessage(): - result.add( - dartantic.ChatMessage( - role: dartantic.ChatMessageRole.model, - parts: _toParts(message.parts), - ), - ); - case genui.AiUiMessage(): - result.add( - dartantic.ChatMessage( - role: dartantic.ChatMessageRole.user, - parts: _toParts(message.parts), - ), - ); - case genui.InternalMessage(): - result.add(dartantic.ChatMessage.system(message.text)); - case genui.ToolResponseMessage(): - result.add( - dartantic.ChatMessage( - role: dartantic.ChatMessageRole.user, - parts: _toolResultPartsToDiParts(message.results), - ), - ); - } - } - } - - return result; - } - - /// Extracts text content from a list of [genui.MessagePart] instances. - /// - /// Joins all [genui.TextPart] text values with newlines. - String _extractText(List parts) { - final textParts = []; - for (final part in parts) { - switch (part) { - case genui.TextPart(): - textParts.add(part.text); - case genui.DataPart(): - if (part.data != null) { - textParts.add('Data: ${jsonEncode(part.data)}'); - } - case genui.ImagePart(): - // Note: dartantic_ai may support images natively in some providers, - // but for simplicity we just note the presence of an image. - if (part.url != null) { - textParts.add('Image at ${part.url}'); - } else { - textParts.add('[Image data]'); - } - case genui.ToolCallPart(): - textParts.add( - 'ToolCall(${part.toolName}): ${jsonEncode(part.arguments)}', - ); - case genui.ToolResultPart(): - textParts.add('ToolResult(${part.callId}): ${part.result}'); - case genui.ThinkingPart(): - textParts.add('Thinking: ${part.text}'); - } - } - return textParts.join('\n'); - } - - /// Converts tool response parts to a textual form for prompts. - String _extractToolResponseText(List results) { - return results - .map((r) => 'ToolResult(${r.callId}): ${r.result}') - .join('\n'); - } - - /// Converts GenUI message parts to dartantic parts. - List _toParts(List parts) { - final converted = []; - for (final part in parts) { - switch (part) { - case genui.TextPart(): - converted.add(dartantic.TextPart(part.text)); - case genui.DataPart(): - final Map? data = part.data; - if (data != null) { - converted.add( - dartantic.DataPart( - utf8.encode(jsonEncode(data)), - mimeType: 'application/json', - ), - ); - } - case genui.ImagePart(): - if (part.url != null) { - converted.add( - dartantic.LinkPart( - part.url!, - mimeType: part.mimeType, - name: null, - ), - ); - } else if (part.base64 != null) { - converted.add( - dartantic.DataPart( - base64Decode(part.base64!), - mimeType: part.mimeType, - ), - ); - } else if (part.bytes != null) { - converted.add( - dartantic.DataPart(part.bytes!, mimeType: part.mimeType), - ); - } else { - converted.add(const dartantic.TextPart('[Image data]')); - } - case genui.ToolCallPart(): - converted.add( - dartantic.ToolPart.call( - callId: part.id, - toolName: part.toolName, - arguments: part.arguments, - ), - ); - case genui.ToolResultPart(): - converted.add( - dartantic.ToolPart.result( - callId: part.callId, - toolName: 'tool_response', - result: _decodeMaybeJson(part.result), - ), - ); - case genui.ThinkingPart(): - converted.add(dartantic.TextPart('Thinking: ${part.text}')); - } - } - return converted; - } - - /// Converts GenUI message parts to dartantic parts, excluding text parts to - /// avoid duplicate text in both prompt and attachments. - List _toPartsWithoutText(List parts) { - final List filtered = parts - .where((p) => p is! genui.TextPart) - .toList(); - return _toParts(filtered); - } - - /// Converts tool result parts from GenUI to dartantic ToolParts. - List _toolResultPartsToDiParts( - List results, - ) { - return results - .map( - (r) => dartantic.ToolPart.result( - callId: r.callId, - toolName: 'tool_response', - result: _decodeMaybeJson(r.result), - ), - ) - .toList(); - } - - /// Attempts to decode a JSON string; returns the original string on failure. - dynamic _decodeMaybeJson(String input) { - try { - return jsonDecode(input); - } catch (_) { - return input; - } - } -} diff --git a/packages/genui_dartantic/lib/src/dartantic_content_generator.dart b/packages/genui_dartantic/lib/src/dartantic_content_generator.dart deleted file mode 100644 index 20e61ce35..000000000 --- a/packages/genui_dartantic/lib/src/dartantic_content_generator.dart +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: specify_nonobvious_local_variable_types - -import 'dart:async'; - -import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; -import 'package:flutter/foundation.dart'; -import 'package:genui/genui.dart'; -import 'package:json_schema_builder/json_schema_builder.dart' as jsb; - -import 'dartantic_content_converter.dart'; - -/// A [ContentGenerator] that uses Dartantic AI to generate content. -/// -/// This generator utilizes a [dartantic.Provider] to interact with various -/// AI providers (OpenAI, Anthropic, Google, Mistral, Cohere, Ollama) through -/// the dartantic_ai package. -/// -/// The generator creates tools from the GenUI catalog and any additional tools -/// provided, then uses dartantic's built-in tool calling and structured output -/// capabilities to generate UI content. -/// -/// This implementation is **stateless** - it does not maintain internal -/// conversation history. Instead, it uses the history provided by -/// [GenUiConversation] via the [sendRequest] method's `history` parameter. -class DartanticContentGenerator implements ContentGenerator { - /// Creates a [DartanticContentGenerator] instance. - /// - /// - [provider]: The dartantic AI provider to use (e.g., `Providers.google`, - /// `Providers.openai`, `Providers.anthropic`). - /// - [catalog]: The catalog of UI components available to the AI. - /// - [systemInstruction]: Optional system instruction for the AI model. - - /// - [additionalTools]: Additional GenUI [AiTool] instances to make - /// available. - DartanticContentGenerator({ - required dartantic.Provider provider, - required this.catalog, - this.systemInstruction, - this.modelName, - List> additionalTools = const [], - }) { - // Build GenUI tools - final genUiTools = >[ - SurfaceUpdateTool( - handleMessage: _a2uiMessageController.add, - catalog: catalog, - ), - BeginRenderingTool(handleMessage: _a2uiMessageController.add), - DeleteSurfaceTool(handleMessage: _a2uiMessageController.add), - ...additionalTools, - ]; - - // Convert all tools to dartantic format - final List dartanticTools = _convertTools(genUiTools); - - // Create agent with converted tools - _agent = dartantic.Agent.forProvider( - provider, - chatModelName: modelName, - tools: dartanticTools, - ); - - // Create additional system instructions to augment what the client sends - _extraInstructions = - ''' - -${dartanticTools.map((tool) => tool.toJson()).join('\n\n')} - - - -${_outputSchema.toJson()} - -'''; - - genUiLogger.info('Extra system instructions: $_extraInstructions'); - } - - /// The catalog of UI components available to the AI. - final Catalog catalog; - - /// The system instruction to use for the AI model. - final String? systemInstruction; - - /// The model name to use. - final String? modelName; - - /// The configuration of the GenUI system. - - late final dartantic.Agent _agent; - final DartanticContentConverter _converter = DartanticContentConverter(); - - final _a2uiMessageController = StreamController.broadcast(); - final _textResponseController = StreamController.broadcast(); - final _errorController = StreamController.broadcast(); - final _isProcessing = ValueNotifier(false); - late final String _extraInstructions; - - /// Structured output schema: a simple object with a required string response. - static final jsb.Schema _outputSchema = jsb.S.object( - properties: { - 'response': jsb.S.string(description: 'The text response to the user.'), - }, - required: ['response'], - ); - - @override - Stream get a2uiMessageStream => _a2uiMessageController.stream; - - @override - Stream get textResponseStream => _textResponseController.stream; - - @override - Stream get errorStream => _errorController.stream; - - @override - ValueListenable get isProcessing => _isProcessing; - - @override - void dispose() { - _a2uiMessageController.close(); - _textResponseController.close(); - _errorController.close(); - _isProcessing.dispose(); - } - - @override - Future sendRequest( - ChatMessage message, { - Iterable? history, - A2UiClientCapabilities? clientCapabilities, - }) async { - _isProcessing.value = true; - try { - // Convert GenUI history to dartantic ChatMessage list - final List dartanticHistory = _converter.toHistory( - history, - systemInstruction: '$systemInstruction\n\n$_extraInstructions', - ); - - // Convert the current GenUI message into prompt text plus parts so we - // preserve text, data, and tool content. - final ({String prompt, List parts}) promptAndParts = - _converter.toPromptAndParts(message); - - // We should never have tool calls or results in request message. - assert(promptAndParts.parts.every((part) => part is! dartantic.ToolPart)); - - genUiLogger.info( - 'Sending request to Dartantic: "${promptAndParts.prompt}"', - ); - genUiLogger.fine('History contains ${dartanticHistory.length} messages'); - - // Use Agent.sendFor with structured output so the model returns a single - // response string instead of dumping JSON/tool content as text. - final dartantic.ChatResult> result = await _agent - .sendFor>( - promptAndParts.prompt, - outputSchema: _outputSchema, - history: dartanticHistory, - attachments: promptAndParts.parts, - ); - - final String responseText = _parseResponse(result.output); - - _textResponseController.add(responseText); - genUiLogger.info('Received response from Dartantic: $responseText'); - } catch (e, st) { - genUiLogger.severe('Error generating content', e, st); - _errorController.add(ContentGeneratorError(e, st)); - } finally { - _isProcessing.value = false; - } - } - - /// Converts GenUI [AiTool] instances to dartantic [dartantic.Tool] instances. - List _convertTools(List> tools) => tools - .map( - (aiTool) => dartantic.Tool( - name: aiTool.name, - description: aiTool.description, - inputSchema: aiTool.parameters, - onCall: (Map args) async { - genUiLogger.fine('Invoking tool: ${aiTool.name} with args: $args'); - final JsonMap result = await aiTool.invoke(args); - genUiLogger.fine('Tool ${aiTool.name} returned: $result'); - return result; - }, - ), - ) - .toList(); - - /// Validates and extracts the response text from the structured output. - String _parseResponse(Map output) { - final Object? responseValue = output['response']; - if (responseValue is! String) { - throw StateError( - 'Dartantic returned a non-string response: $responseValue', - ); - } - final String responseText = responseValue.trim(); - if (responseText.isEmpty) { - throw StateError('Dartantic returned an empty response string.'); - } - return responseText; - } -} diff --git a/packages/genui_dartantic/lib/src/dartantic_schema_adapter.dart b/packages/genui_dartantic/lib/src/dartantic_schema_adapter.dart deleted file mode 100644 index 69ac64cc2..000000000 --- a/packages/genui_dartantic/lib/src/dartantic_schema_adapter.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:json_schema/json_schema.dart'; -import 'package:json_schema_builder/json_schema_builder.dart' as jsb; - -/// Converts a [jsb.Schema] from the `json_schema_builder` package to a -/// [JsonSchema] from the `json_schema` package. -/// -/// This is a simple pass-through conversion since both packages represent -/// JSON Schema - the dartantic provider handles any provider-specific -/// limitations. -JsonSchema? adaptSchema(jsb.Schema? schema) { - if (schema == null) return null; - return JsonSchema.create(schema.value); -} diff --git a/packages/genui_dartantic/pubspec.yaml b/packages/genui_dartantic/pubspec.yaml deleted file mode 100644 index bc8968aed..000000000 --- a/packages/genui_dartantic/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: genui_dartantic -description: Integration package for genui and Dartantic AI. -version: 0.7.0 -homepage: https://github.com/flutter/genui/tree/main/packages/genui_dartantic -license: BSD-3-Clause -issue_tracker: https://github.com/flutter/genui/issues - -resolution: workspace - -environment: - sdk: ">=3.9.2 <4.0.0" - flutter: ">=3.35.7 <4.0.0" - -dependencies: - dartantic_ai: ^3.1.0 - flutter: - sdk: flutter - genui: ^0.7.0 - json_schema: ^5.2.0 - json_schema_builder: ^0.1.3 - -dev_dependencies: - flutter_test: - sdk: flutter - logging: ^1.3.0 diff --git a/packages/genui_dartantic/test/dartantic_content_converter_test.dart b/packages/genui_dartantic/test/dartantic_content_converter_test.dart deleted file mode 100644 index f7469ed74..000000000 --- a/packages/genui_dartantic/test/dartantic_content_converter_test.dart +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart' as genui; -import 'package:genui_dartantic/genui_dartantic.dart'; - -void main() { - group('DartanticContentConverter', () { - late DartanticContentConverter converter; - - setUp(() { - converter = DartanticContentConverter(); - }); - - group('toPromptAndParts', () { - test('converts UserMessage with text to prompt string', () { - final message = genui.UserMessage.text('Hello, world!'); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, 'Hello, world!'); - // Text parts are excluded from parts to avoid duplication (text is in - // prompt) - expect(result.parts, isEmpty); - }); - - test('converts UserMessage with multiple text parts', () { - final message = genui.UserMessage([ - const genui.TextPart('First part'), - const genui.TextPart('Second part'), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, 'First part\nSecond part'); - // Text parts are excluded from parts to avoid duplication (text is in - // prompt) - expect(result.parts, isEmpty); - }); - - test('converts UserUiInteractionMessage to prompt string', () { - final message = genui.UserUiInteractionMessage.text('UI interaction'); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, 'UI interaction'); - }); - - test('converts AiTextMessage to prompt string', () { - final message = genui.AiTextMessage.text('AI response'); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, 'AI response'); - }); - - test('converts InternalMessage to prompt string', () { - const message = genui.InternalMessage('System instruction'); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, 'System instruction'); - // InternalMessage returns empty parts (text is in prompt only) - expect(result.parts, isEmpty); - }); - - test('handles ToolResponseMessage', () { - const message = genui.ToolResponseMessage([ - genui.ToolResultPart(callId: 'call1', result: '{"status": "ok"}'), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('ToolResult(call1)')); - expect(result.parts, isNotEmpty); - }); - - test('handles DataPart in message', () { - final message = genui.UserMessage([ - const genui.TextPart('Check this data:'), - const genui.DataPart({'key': 'value'}), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('Check this data:')); - expect(result.prompt, contains('Data:')); - expect( - result.parts.whereType().length, - greaterThanOrEqualTo(1), - ); - }); - - test('handles ImagePart with URL in message', () { - final message = genui.UserMessage([ - const genui.TextPart('Look at this image:'), - genui.ImagePart.fromUrl( - Uri.parse('https://example.com/image.png'), - mimeType: 'image/png', - ), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('Look at this image:')); - expect( - result.prompt, - contains('Image at https://example.com/image.png'), - ); - expect(result.parts.whereType(), isNotEmpty); - }); - - test('handles ThinkingPart in message', () { - final message = genui.AiTextMessage([ - const genui.ThinkingPart('Let me think about this...'), - const genui.TextPart('Here is my answer.'), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('Thinking: Let me think about this...')); - expect(result.prompt, contains('Here is my answer.')); - }); - - test('includes ToolCallPart in prompt', () { - final message = genui.AiTextMessage([ - const genui.TextPart('Calling a tool'), - const genui.ToolCallPart( - id: 'call1', - toolName: 'test_tool', - arguments: {'arg': 'value'}, - ), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('ToolCall(test_tool)')); - expect(result.parts.whereType(), isNotEmpty); - }); - - test('includes ToolResultPart in prompt', () { - final message = genui.AiTextMessage([ - const genui.TextPart('Got result'), - const genui.ToolResultPart(callId: 'call1', result: '{}'), - ]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, contains('ToolResult(call1)')); - expect(result.parts.whereType(), isNotEmpty); - }); - - test('handles empty message parts', () { - final message = genui.UserMessage([]); - - final ({String prompt, List parts}) result = converter - .toPromptAndParts(message); - - expect(result.prompt, ''); - }); - }); - - group('toHistory', () { - test('returns empty list for null history', () { - final List result = converter.toHistory(null); - - expect(result, isEmpty); - }); - - test('returns empty list for empty history', () { - final List result = converter.toHistory([]); - - expect(result, isEmpty); - }); - - test('includes system instruction as first message', () { - final List result = converter.toHistory( - null, - systemInstruction: 'You are a helpful assistant.', - ); - - expect(result, hasLength(1)); - expect(result[0].role, dartantic.ChatMessageRole.system); - expect(result[0].text, 'You are a helpful assistant.'); - }); - - test('converts UserMessage to user role', () { - final history = [genui.UserMessage.text('Hello')]; - - final List result = converter.toHistory(history); - - expect(result, hasLength(1)); - expect(result[0].role, dartantic.ChatMessageRole.user); - expect(result[0].text, 'Hello'); - }); - - test('converts UserUiInteractionMessage to user role', () { - final history = [genui.UserUiInteractionMessage.text('Clicked button')]; - - final List result = converter.toHistory(history); - - expect(result, hasLength(1)); - expect(result[0].role, dartantic.ChatMessageRole.user); - expect(result[0].text, 'Clicked button'); - }); - - test('converts AiTextMessage to model role', () { - final history = [genui.AiTextMessage.text('AI response')]; - - final List result = converter.toHistory(history); - - expect(result, hasLength(1)); - expect(result[0].role, dartantic.ChatMessageRole.model); - expect(result[0].text, 'AI response'); - }); - - test('includes InternalMessage as system', () { - final List history = [ - genui.UserMessage.text('Hello'), - const genui.InternalMessage('Internal note'), - genui.AiTextMessage.text('Response'), - ]; - - final List result = converter.toHistory(history); - - expect(result, hasLength(3)); - expect(result[0].role, dartantic.ChatMessageRole.user); - expect(result[1].role, dartantic.ChatMessageRole.system); - expect(result[2].role, dartantic.ChatMessageRole.model); - }); - - test('includes ToolResponseMessage as user tool results', () { - final List history = [ - genui.UserMessage.text('Hello'), - const genui.ToolResponseMessage([ - genui.ToolResultPart(callId: 'call1', result: '{}'), - ]), - genui.AiTextMessage.text('Response'), - ]; - - final List result = converter.toHistory(history); - - expect(result, hasLength(3)); - expect(result[0].role, dartantic.ChatMessageRole.user); - expect(result[1].role, dartantic.ChatMessageRole.user); - expect( - result[1].parts.whereType().length, - greaterThanOrEqualTo(1), - ); - expect(result[2].role, dartantic.ChatMessageRole.model); - }); - - test('handles full conversation with system instruction', () { - final List history = [ - genui.UserMessage.text('What is 2+2?'), - genui.AiTextMessage.text('2+2 equals 4.'), - genui.UserMessage.text('And 3+3?'), - ]; - - final List result = converter.toHistory( - history, - systemInstruction: 'You are a math tutor.', - ); - - expect(result, hasLength(4)); - expect(result[0].role, dartantic.ChatMessageRole.system); - expect(result[0].text, 'You are a math tutor.'); - expect(result[1].role, dartantic.ChatMessageRole.user); - expect(result[1].text, 'What is 2+2?'); - expect(result[2].role, dartantic.ChatMessageRole.model); - expect(result[2].text, '2+2 equals 4.'); - expect(result[3].role, dartantic.ChatMessageRole.user); - expect(result[3].text, 'And 3+3?'); - }); - }); - }); -} diff --git a/packages/genui_dartantic/test/dartantic_content_generator_test.dart b/packages/genui_dartantic/test/dartantic_content_generator_test.dart deleted file mode 100644 index 9258d445c..000000000 --- a/packages/genui_dartantic/test/dartantic_content_generator_test.dart +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:dartantic_ai/dartantic_ai.dart' as dartantic; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart' as genui; -import 'package:genui_dartantic/genui_dartantic.dart'; - -void main() { - group('DartanticContentGenerator', () { - // Use the ollama provider for testing - it doesn't require API keys - // Note: These tests only verify object construction, not actual AI calls - late dartantic.OllamaProvider testProvider; - - setUp(() { - testProvider = dartantic.OllamaProvider(); - }); - - group('construction', () { - test('creates generator with required parameters', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - ); - - expect(generator, isNotNull); - expect(generator.catalog, isNotNull); - expect(generator.isProcessing.value, isFalse); - - generator.dispose(); - }); - - test('creates generator with all optional parameters', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - systemInstruction: 'You are a helpful assistant.', - additionalTools: [ - genui.DynamicAiTool>( - name: 'testTool', - description: 'A test tool', - invokeFunction: (args) async => {'result': 'ok'}, - ), - ], - ); - - expect(generator, isNotNull); - expect(generator.systemInstruction, 'You are a helpful assistant.'); - - generator.dispose(); - }); - }); - - group('streams', () { - test('provides a2uiMessageStream', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - ); - - expect(generator.a2uiMessageStream, isA>()); - - generator.dispose(); - }); - - test('provides textResponseStream', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - ); - - expect(generator.textResponseStream, isA>()); - - generator.dispose(); - }); - - test('provides errorStream', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - ); - - expect( - generator.errorStream, - isA>(), - ); - - generator.dispose(); - }); - }); - - group('isProcessing', () { - test('initially false', () { - final generator = DartanticContentGenerator( - provider: testProvider, - catalog: const genui.Catalog({}), - ); - - expect(generator.isProcessing.value, isFalse); - - generator.dispose(); - }); - }); - }); -} diff --git a/packages/genui_dartantic/test/dartantic_schema_adapter_test.dart b/packages/genui_dartantic/test/dartantic_schema_adapter_test.dart deleted file mode 100644 index 267b6f4b0..000000000 --- a/packages/genui_dartantic/test/dartantic_schema_adapter_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: specify_nonobvious_local_variable_types - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui_dartantic/genui_dartantic.dart'; -import 'package:json_schema_builder/json_schema_builder.dart' as jsb; - -void main() { - group('adaptSchema', () { - test('returns null for null input', () { - expect(adaptSchema(null), isNull); - }); - - test('converts object schema', () { - final schema = jsb.Schema.object( - properties: { - 'name': jsb.Schema.string(description: 'The name.'), - 'age': jsb.Schema.integer(), - }, - required: ['name'], - description: 'A person.', - ); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'object'); - expect(result.schemaMap!['description'], 'A person.'); - expect(result.schemaMap!['required'], ['name']); - }); - - test('converts array schema', () { - final schema = jsb.Schema.list( - items: jsb.Schema.string(), - minItems: 1, - maxItems: 10, - ); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'array'); - expect(result.schemaMap!['minItems'], 1); - expect(result.schemaMap!['maxItems'], 10); - }); - - test('converts string schema with enum', () { - final schema = jsb.Schema.string( - enumValues: ['a', 'b', 'c'], - format: 'email', - ); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'string'); - expect(result.schemaMap!['enum'], ['a', 'b', 'c']); - expect(result.schemaMap!['format'], 'email'); - }); - - test('converts number schema', () { - final schema = jsb.Schema.number(minimum: 0, maximum: 100); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'number'); - expect(result.schemaMap!['minimum'], 0); - expect(result.schemaMap!['maximum'], 100); - }); - - test('converts integer schema', () { - final schema = jsb.Schema.integer(minimum: 0, maximum: 100); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'integer'); - }); - - test('converts boolean schema', () { - final schema = jsb.Schema.boolean(); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'boolean'); - }); - - test('converts nested schemas', () { - final schema = jsb.Schema.object( - properties: { - 'user': jsb.Schema.object( - properties: {'tags': jsb.Schema.list(items: jsb.Schema.string())}, - ), - }, - ); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['type'], 'object'); - }); - - test('preserves anyOf', () { - final schema = jsb.Schema.combined( - anyOf: [ - {'type': 'string'}, - {'type': 'integer'}, - ], - ); - - final result = adaptSchema(schema); - - expect(result, isNotNull); - expect(result!.schemaMap!['anyOf'], isA>()); - }); - }); -} diff --git a/packages/genui_firebase_ai/CHANGELOG.md b/packages/genui_firebase_ai/CHANGELOG.md deleted file mode 100644 index f89273d3a..000000000 --- a/packages/genui_firebase_ai/CHANGELOG.md +++ /dev/null @@ -1,26 +0,0 @@ -# `genui_firebase_ai` Changelog - -## 0.7.1 (in progress) - -## 0.7.0 - -- Updated version to match `genui` package version. - -## 0.6.1 - -- Updated version to match `genui` package version. - -## 0.6.0 - -- **BREAKING**: Removed `GenUiConfiguration`. -- Updated to work with `genui` 0.6.0 (`A2uiMessageProcessor` rename). -- `BeginRenderingTool` now sends `catalogId`. - - -## 0.5.1 - -- Homepage URL was updated. - -## 0.5.0 - -- Initial published release. diff --git a/packages/genui_firebase_ai/LICENSE b/packages/genui_firebase_ai/LICENSE deleted file mode 100644 index 33e1140da..000000000 --- a/packages/genui_firebase_ai/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2025 The Flutter Authors. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/genui_firebase_ai/README.md b/packages/genui_firebase_ai/README.md deleted file mode 100644 index 513ecadfe..000000000 --- a/packages/genui_firebase_ai/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# genui_firebase_ai - -This package provides the integration between `genui` and the Firebase AI Logic SDK. It allows you to use the power of Google's Gemini models to generate dynamic user interfaces in your Flutter applications. - -## Features - -- **FirebaseAiContentGenerator:** An implementation of `ContentGenerator` that connects to the Firebase AI SDK. -- **GeminiContentConverter:** Converts between the generic `ChatMessage` and the `firebase_ai` specific `Content` classes. -- **GeminiSchemaAdapter:** Adapts schemas from `json_schema_builder` to the `firebase_ai` format. -- **GeminiGenerativeModelInterface:** An interface for the generative model to allow for mock implementations, primarily for testing. -- **Additional Tools:** Supports adding custom `AiTool`s to extend the AI's capabilities via the `additionalTools` parameter. -- **Error Handling:** Exposes an `errorStream` to listen for and handle any errors during content generation. - -## Getting Started - -To use this package, you will need to have a Firebase project set up and the Firebase AI SDK configured. - -Then, you can create an instance of `FirebaseAiContentGenerator` and pass it to your `GenUiConversation`: - -```dart -final catalog = CoreCatalogItems.asCatalog(); -final a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [catalog]); -// Example of a custom tool -final myCustomTool = DynamicAiTool>( - name: 'my_custom_action', - description: 'Performs a custom action.', - parameters: dsb.S.object(properties: { - 'detail': dsb.S.string(), - }), - invokeFunction: (args) async { - print('Custom action called with: $args'); - return {'status': 'ok'}; - }, -); - -final contentGenerator = FirebaseAiContentGenerator( - // model: 'gemini-1.5-pro', // Optional: defaults to gemini-1.5-flash - catalog: catalog, - systemInstruction: 'You are a helpful assistant.', - additionalTools: [myCustomTool], -); -final genUiConversation = GenUiConversation( - a2uiMessageProcessor: a2uiMessageProcessor, - contentGenerator: contentGenerator, - ... -); -``` - -## Notes - -- **Image Handling:** Currently, `ImagePart`s provided with only a `url` (without `bytes` or `base64` data) will be sent to the model as a text description of the URL, as the image data is not automatically fetched by the converter. -- **Token Usage:** The `FirebaseAiContentGenerator` tracks token usage in the `inputTokenUsage` and `outputTokenUsage` properties. diff --git a/packages/genui_firebase_ai/lib/genui_firebase_ai.dart b/packages/genui_firebase_ai/lib/genui_firebase_ai.dart deleted file mode 100644 index 3156fc301..000000000 --- a/packages/genui_firebase_ai/lib/genui_firebase_ai.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/firebase_ai_content_generator.dart'; -export 'src/gemini_content_converter.dart'; -export 'src/gemini_generative_model.dart'; -export 'src/gemini_schema_adapter.dart'; diff --git a/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart b/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart deleted file mode 100644 index a19d1b5e5..000000000 --- a/packages/genui_firebase_ai/lib/src/firebase_ai_content_generator.dart +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; - -import 'package:firebase_ai/firebase_ai.dart' hide TextPart; -// ignore: implementation_imports -import 'package:firebase_ai/src/api.dart' show ModalityTokenCount; -import 'package:flutter/foundation.dart'; -import 'package:genui/genui.dart' hide Part; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -import 'gemini_content_converter.dart'; -import 'gemini_generative_model.dart'; -import 'gemini_schema_adapter.dart'; - -/// A factory for creating a [GeminiGenerativeModelInterface]. -/// -/// This is used to allow for custom model creation, for example, for testing. -typedef GenerativeModelFactory = - GeminiGenerativeModelInterface Function({ - required FirebaseAiContentGenerator configuration, - Content? systemInstruction, - List? tools, - ToolConfig? toolConfig, - }); - -/// A [ContentGenerator] that uses the Firebase AI API to generate content. -/// -/// This generator utilizes a [GeminiGenerativeModelInterface] to interact with -/// the Firebase AI API. The actual model instance is created by the -/// [modelCreator] function, which defaults to [defaultGenerativeModelFactory]. -class FirebaseAiContentGenerator implements ContentGenerator { - /// Creates a [FirebaseAiContentGenerator] instance with specified - /// configurations. - FirebaseAiContentGenerator({ - required this.catalog, - this.systemInstruction, - this.outputToolName = 'provideFinalOutput', - this.modelCreator = defaultGenerativeModelFactory, - this.additionalTools = const [], - }); - - /// The catalog of UI components available to the AI. - final Catalog catalog; - - /// The system instruction to use for the AI model. - final String? systemInstruction; - - /// The name of an internal pseudo-tool used to retrieve the final structured - /// output from the AI. - /// - /// This only needs to be provided in case of name collision with another - /// tool. - /// - /// Defaults to 'provideFinalOutput'. - final String outputToolName; - - /// A function to use for creating the model itself. - /// - /// This factory function is responsible for instantiating the - /// [GeminiGenerativeModelInterface] used for AI interactions. It allows for - /// customization of the model setup, such as using different HTTP clients, or - /// for providing mock models during testing. The factory receives this - /// [FirebaseAiContentGenerator] instance as configuration. - /// - /// Defaults to a wrapper for the regular [GenerativeModel] constructor, - /// [defaultGenerativeModelFactory]. - final GenerativeModelFactory modelCreator; - - /// Additional tools to make available to the AI model. - final List additionalTools; - - /// The total number of input tokens used by this client. - int inputTokenUsage = 0; - - /// The total number of output tokens used by this client - int outputTokenUsage = 0; - - final _a2uiMessageController = StreamController.broadcast(); - final _textResponseController = StreamController.broadcast(); - final _errorController = StreamController.broadcast(); - final _isProcessing = ValueNotifier(false); - - @override - Stream get a2uiMessageStream => _a2uiMessageController.stream; - - @override - Stream get textResponseStream => _textResponseController.stream; - - @override - Stream get errorStream => _errorController.stream; - - @override - ValueListenable get isProcessing => _isProcessing; - - @override - void dispose() { - _a2uiMessageController.close(); - _textResponseController.close(); - _errorController.close(); - _isProcessing.dispose(); - } - - @override - Future sendRequest( - ChatMessage message, { - Iterable? history, - A2UiClientCapabilities? clientCapabilities, - }) async { - _isProcessing.value = true; - try { - final messages = [...?history, message]; - final Object? response = await _generate( - messages: messages, - // This turns on forced function calling. - outputSchema: dsb.S.object(properties: {'response': dsb.S.string()}), - ); - // Convert any response to a text response to the user. - if (response is Map && response.containsKey('response')) { - _textResponseController.add(response['response']! as String); - } - } catch (e, st) { - genUiLogger.severe('Error generating content', e, st); - _errorController.add(ContentGeneratorError(e, st)); - } finally { - _isProcessing.value = false; - } - } - - /// The default factory function for creating a [GenerativeModel]. - /// - /// This function instantiates a standard [GenerativeModel] using the `model` - /// from the provided [FirebaseAiContentGenerator] `configuration`. - static GeminiGenerativeModelInterface defaultGenerativeModelFactory({ - required FirebaseAiContentGenerator configuration, - Content? systemInstruction, - List? tools, - ToolConfig? toolConfig, - }) { - return GeminiGenerativeModel( - FirebaseAI.googleAI().generativeModel( - model: 'gemini-2.5-flash', - systemInstruction: systemInstruction, - tools: tools, - toolConfig: toolConfig, - ), - ); - } - - ({List? generativeAiTools, Set allowedFunctionNames}) - _setupToolsAndFunctions({ - required bool isForcedToolCalling, - required List availableTools, - required GeminiSchemaAdapter adapter, - required dsb.Schema? outputSchema, - }) { - genUiLogger.fine( - 'Setting up tools' - '${isForcedToolCalling ? ' with forced tool calling' : ''}', - ); - // Create an "output" tool that copies its args into the output. - final DynamicAiTool>? finalOutputAiTool = - isForcedToolCalling - ? DynamicAiTool>( - name: outputToolName, - description: - '''Returns the final output. Call this function when you are done with the current turn of the conversation. Do not call this if you need to use other tools first. You MUST call this tool when you are done.''', - // Wrap the outputSchema in an object so that the output schema - // isn't limited to objects. - parameters: dsb.S.object(properties: {'output': outputSchema!}), - invokeFunction: (args) async => args, // Invoke is a pass-through - ) - : null; - - final List> allTools = isForcedToolCalling - ? [...availableTools, finalOutputAiTool!] - : availableTools; - genUiLogger.fine( - 'Available tools: ${allTools.map((t) => t.name).join(', ')}', - ); - - final uniqueAiToolsByName = {}; - final toolFullNames = {}; - for (final tool in allTools) { - if (uniqueAiToolsByName.containsKey(tool.name)) { - throw Exception('Duplicate tool ${tool.name} registered.'); - } - uniqueAiToolsByName[tool.name] = tool; - if (tool.name != tool.fullName) { - if (toolFullNames.contains(tool.fullName)) { - throw Exception('Duplicate tool ${tool.fullName} registered.'); - } - toolFullNames.add(tool.fullName); - } - } - - final functionDeclarations = []; - for (final AiTool tool in uniqueAiToolsByName.values) { - Schema? adaptedParameters; - if (tool.parameters != null) { - final GeminiSchemaAdapterResult result = adapter.adapt( - tool.parameters!, - ); - if (result.errors.isNotEmpty) { - genUiLogger.warning( - 'Errors adapting parameters for tool ${tool.name}: ' - '${result.errors.join('\n')}', - ); - } - adaptedParameters = result.schema; - } - final Map? parameters = adaptedParameters?.properties; - functionDeclarations.add( - FunctionDeclaration( - tool.name, - tool.description, - parameters: parameters ?? const {}, - ), - ); - if (tool.name != tool.fullName) { - functionDeclarations.add( - FunctionDeclaration( - tool.fullName, - tool.description, - parameters: parameters ?? const {}, - ), - ); - } - } - genUiLogger.fine( - 'Adapted tools to function declarations: ' - '${functionDeclarations.map((d) => d.name).join(', ')}', - ); - - final List? generativeAiTools = functionDeclarations.isNotEmpty - ? [Tool.functionDeclarations(functionDeclarations)] - : null; - - if (generativeAiTools != null) { - genUiLogger.finest( - 'Tool declarations being sent to the model: ' - '${jsonEncode(generativeAiTools)}', - ); - } - - final allowedFunctionNames = { - ...uniqueAiToolsByName.keys, - ...toolFullNames, - }; - - genUiLogger.fine( - 'Allowed function names for model: ${allowedFunctionNames.join(', ')}', - ); - - return ( - generativeAiTools: generativeAiTools, - allowedFunctionNames: allowedFunctionNames, - ); - } - - Future< - ({List functionResponseParts, Object? capturedResult}) - > - _processFunctionCalls({ - required List functionCalls, - required bool isForcedToolCalling, - required List availableTools, - Object? capturedResult, - }) async { - genUiLogger.fine( - 'Processing ${functionCalls.length} function calls from model.', - ); - final functionResponseParts = []; - for (final call in functionCalls) { - genUiLogger.fine( - 'Processing function call: ${call.name} with args: ${call.args}', - ); - if (isForcedToolCalling && call.name == outputToolName) { - try { - capturedResult = call.args['output']; - genUiLogger.fine( - 'Captured final output from tool "$outputToolName".', - ); - } catch (exception, stack) { - genUiLogger.severe( - 'Unable to read output: $call [${call.args}]', - exception, - stack, - ); - } - genUiLogger.info( - '****** Gen UI Output ******.\n' - '${const JsonEncoder.withIndent(' ').convert(capturedResult)}', - ); - break; - } - - final AiTool aiTool = availableTools.firstWhere( - (t) => t.name == call.name || t.fullName == call.name, - orElse: () => throw Exception('Unknown tool ${call.name} called.'), - ); - Map toolResult; - try { - genUiLogger.fine('Invoking tool: ${aiTool.name}'); - toolResult = await aiTool.invoke(call.args); - genUiLogger.info( - 'Invoked tool ${aiTool.name} with args ${call.args}. ' - 'Result: $toolResult', - ); - } catch (exception, stack) { - genUiLogger.severe( - 'Error invoking tool ${aiTool.name} with args ${call.args}: ', - exception, - stack, - ); - toolResult = { - 'error': 'Tool ${aiTool.name} failed to execute: $exception', - }; - } - functionResponseParts.add(FunctionResponse(call.name, toolResult)); - } - genUiLogger.fine( - 'Finished processing function calls. Returning ' - '${functionResponseParts.length} responses.', - ); - return ( - functionResponseParts: functionResponseParts, - capturedResult: capturedResult, - ); - } - - Future _generate({ - required Iterable messages, - dsb.Schema? outputSchema, - }) async { - final isForcedToolCalling = outputSchema != null; - final converter = GeminiContentConverter(); - final adapter = GeminiSchemaAdapter(); - - final List> availableTools = [ - SurfaceUpdateTool( - handleMessage: _a2uiMessageController.add, - catalog: catalog, - ), - BeginRenderingTool( - handleMessage: _a2uiMessageController.add, - catalogId: catalog.catalogId, - ), - DeleteSurfaceTool(handleMessage: _a2uiMessageController.add), - ...additionalTools, - ]; - - // A local copy of the incoming messages which is updated with tool results - // as they are generated. - final List mutableContent = converter.toFirebaseAiContent( - messages, - ); - - final ( - :List? generativeAiTools, - :Set allowedFunctionNames, - ) = _setupToolsAndFunctions( - isForcedToolCalling: isForcedToolCalling, - availableTools: availableTools, - adapter: adapter, - outputSchema: outputSchema, - ); - - var toolUsageCycle = 0; - const maxToolUsageCycles = 40; // Safety break for tool loops - Object? capturedResult; - - final GeminiGenerativeModelInterface model = modelCreator( - configuration: this, - systemInstruction: systemInstruction == null - ? null - : Content.system(systemInstruction!), - tools: generativeAiTools, - toolConfig: isForcedToolCalling - ? ToolConfig( - functionCallingConfig: FunctionCallingConfig.any( - allowedFunctionNames.toSet(), - ), - ) - : ToolConfig(functionCallingConfig: FunctionCallingConfig.auto()), - ); - - while (toolUsageCycle < maxToolUsageCycles) { - genUiLogger.fine('Starting tool usage cycle ${toolUsageCycle + 1}.'); - if (isForcedToolCalling && capturedResult != null) { - genUiLogger.fine('Captured result found, exiting tool usage loop.'); - break; - } - toolUsageCycle++; - - final String concatenatedContents = mutableContent - .map((c) => const JsonEncoder.withIndent(' ').convert(c.toJson())) - .join('\n'); - - genUiLogger.info( - '''****** Performing Inference ******\n$concatenatedContents -With functions: - '${allowedFunctionNames.join(', ')}', - ''', - ); - final inferenceStartTime = DateTime.now(); - GenerateContentResponse response; - - response = await model.generateContent(mutableContent); - genUiLogger.finest('Raw model response: ${_responseToString(response)}'); - - final Duration elapsed = DateTime.now().difference(inferenceStartTime); - - if (response.usageMetadata != null) { - inputTokenUsage += response.usageMetadata!.promptTokenCount ?? 0; - outputTokenUsage += response.usageMetadata!.candidatesTokenCount ?? 0; - } - genUiLogger.info( - '****** Completed Inference ******\n' - 'Latency = ${elapsed.inMilliseconds}ms\n' - 'Output tokens = ${response.usageMetadata?.candidatesTokenCount ?? 0}\n' - 'Prompt tokens = ${response.usageMetadata?.promptTokenCount ?? 0}', - ); - - if (response.candidates.isEmpty) { - genUiLogger.warning( - 'Response has no candidates: ${response.promptFeedback}', - ); - return isForcedToolCalling ? null : ''; - } - - final Candidate candidate = response.candidates.first; - final List functionCalls = candidate.content.parts - .whereType() - .toList(); - - if (functionCalls.isEmpty) { - genUiLogger.fine('Model response contained no function calls.'); - if (isForcedToolCalling) { - genUiLogger.warning( - 'Model did not call any function. FinishReason: ' - '${candidate.finishReason}. Text: "${candidate.text}" ', - ); - if (candidate.text != null && candidate.text!.trim().isNotEmpty) { - genUiLogger.warning( - 'Model returned direct text instead of a tool call. This might ' - 'be an error or unexpected AI behavior for forced tool calling.', - ); - } - genUiLogger.fine( - 'Model returned text but no function calls with forced tool ' - 'calling, so returning null.', - ); - return null; - } else { - final String text = candidate.text ?? ''; - mutableContent.add(candidate.content); - genUiLogger.fine('Returning text response: "$text"'); - _textResponseController.add(text); - return text; - } - } - - genUiLogger.fine( - 'Model response contained ${functionCalls.length} function calls.', - ); - mutableContent.add(candidate.content); - genUiLogger.fine( - 'Added assistant message with ${candidate.content.parts.length} ' - 'parts to conversation.', - ); - - final ({ - Object? capturedResult, - List functionResponseParts, - }) - result = await _processFunctionCalls( - functionCalls: functionCalls, - isForcedToolCalling: isForcedToolCalling, - availableTools: availableTools, - capturedResult: capturedResult, - ); - capturedResult = result.capturedResult; - final List functionResponseParts = - result.functionResponseParts; - - if (functionResponseParts.isNotEmpty) { - mutableContent.add(Content.functionResponses(functionResponseParts)); - genUiLogger.fine( - 'Added tool response message with ${functionResponseParts.length} ' - 'parts to conversation.', - ); - } - - // If the model returned a text response, we assume it's the final - // response and we should stop the tool calling loop. - if (!isForcedToolCalling && - candidate.text != null && - candidate.text!.trim().isNotEmpty) { - genUiLogger.fine( - 'Model returned a text response of "${candidate.text!.trim()}". ' - 'Exiting tool loop.', - ); - _textResponseController.add(candidate.text!); - return candidate.text; - } - } - - if (isForcedToolCalling) { - if (toolUsageCycle >= maxToolUsageCycles) { - genUiLogger.severe( - 'Error: Tool usage cycle exceeded maximum of $maxToolUsageCycles. ', - 'No final output was produced.', - StackTrace.current, - ); - } - genUiLogger.fine('Exited tool usage loop. Returning captured result.'); - return capturedResult; - } else { - genUiLogger.severe( - 'Error: Tool usage cycle exceeded maximum of $maxToolUsageCycles. ', - 'No final output was produced.', - StackTrace.current, - ); - return ''; - } - } -} - -String _usageMetadata(UsageMetadata? metadata) { - if (metadata == null) return ''; - final buffer = StringBuffer(); - buffer.writeln('UsageMetadata('); - buffer.writeln(' promptTokenCount: ${metadata.promptTokenCount},'); - buffer.writeln(' candidatesTokenCount: ${metadata.candidatesTokenCount},'); - buffer.writeln(' totalTokenCount: ${metadata.totalTokenCount},'); - buffer.writeln(' thoughtsTokenCount: ${metadata.thoughtsTokenCount},'); - buffer.writeln( - ' toolUsePromptTokenCount: ${metadata.toolUsePromptTokenCount},', - ); - buffer.writeln(' promptTokensDetails: ['); - for (final ModalityTokenCount detail - in metadata.promptTokensDetails ?? []) { - buffer.writeln(' ModalityTokenCount('); - buffer.writeln(' modality: ${detail.modality},'); - buffer.writeln(' tokenCount: ${detail.tokenCount},'); - buffer.writeln(' ),'); - } - buffer.writeln(' ],'); - buffer.writeln(' candidatesTokensDetails: ['); - for (final ModalityTokenCount detail - in metadata.candidatesTokensDetails ?? []) { - buffer.writeln(' ModalityTokenCount('); - buffer.writeln(' ${detail.modality},'); - buffer.writeln(' ${detail.tokenCount},'); - buffer.writeln(' ),'); - } - buffer.writeln(' ],'); - buffer.writeln(' toolUsePromptTokensDetails: ['); - for (final ModalityTokenCount detail - in metadata.toolUsePromptTokensDetails ?? []) { - buffer.writeln(' ModalityTokenCount('); - buffer.writeln(' ${detail.modality},'); - buffer.writeln(' ${detail.tokenCount},'); - buffer.writeln(' ),'); - } - buffer.writeln(' ],'); - buffer.writeln(')'); - return buffer.toString(); -} - -String _responseToString(GenerateContentResponse response) { - final buffer = StringBuffer(); - buffer.writeln('GenerateContentResponse('); - buffer.writeln(' usageMetadata: ${_usageMetadata(response.usageMetadata)},'); - buffer.writeln(' promptFeedback: ${response.promptFeedback},'); - buffer.writeln(' candidates: ['); - for (final Candidate candidate in response.candidates) { - buffer.writeln(' Candidate('); - buffer.writeln(' finishReason: ${candidate.finishReason},'); - buffer.writeln(' finishMessage: "${candidate.finishMessage}",'); - buffer.writeln(' content: Content('); - buffer.writeln(' role: "${candidate.content.role}",'); - buffer.writeln(' parts: ['); - for (final Part part in candidate.content.parts) { - if (part is TextPart) { - buffer.writeln( - ' TextPart(text: "${(part as TextPart).text}"),', - ); - } else if (part is FunctionCall) { - buffer.writeln(' FunctionCall('); - buffer.writeln(' name: "${part.name}",'); - final String indentedLines = (const JsonEncoder.withIndent( - ' ', - ).convert(part.args)).split('\n').join('\n '); - buffer.writeln(' args: $indentedLines,'); - buffer.writeln(' ),'); - } else { - buffer.writeln(' Unknown Part: ${part.runtimeType},'); - } - } - buffer.writeln(' ],'); - buffer.writeln(' ),'); - buffer.writeln(' ),'); - } - buffer.writeln(' ],'); - buffer.writeln(')'); - return buffer.toString(); -} diff --git a/packages/genui_firebase_ai/lib/src/gemini_content_converter.dart b/packages/genui_firebase_ai/lib/src/gemini_content_converter.dart deleted file mode 100644 index 85ad6bb4f..000000000 --- a/packages/genui_firebase_ai/lib/src/gemini_content_converter.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -import 'package:firebase_ai/firebase_ai.dart' as firebase_ai; -import 'package:genui/genui.dart'; - -/// An exception thrown by this package. -class ContentConverterException implements Exception { - /// Creates an [ContentConverterException] with the given [message]. - ContentConverterException(this.message); - - /// The message associated with the exception. - final String message; - - @override - String toString() => '$ContentConverterException: $message'; -} - -/// A class to convert between the generic `ChatMessage` and the `firebase_ai` -/// specific `Content` classes. -/// -/// This class is responsible for translating the abstract [ChatMessage] -/// representation into the concrete `firebase_ai.Content` representation -/// required by the `firebase_ai` package. -/// -/// **Note on Image Handling:** [ImagePart] instances that are provided with -/// only a `url` (and no `bytes` or `base64` data) will be converted to a -/// simple text representation of the URL (e.g., "Image at {url}"). The image -/// data is not automatically fetched from the URL by this converter. -class GeminiContentConverter { - /// Converts a list of `ChatMessage` objects to a list of - /// `firebase_ai.Content` objects. - List toFirebaseAiContent( - Iterable messages, - ) { - final result = []; - for (final message in messages) { - final (String? role, List parts) = switch (message) { - UserMessage() => ('user', _convertParts(message.parts)), - UserUiInteractionMessage() => ('user', _convertParts(message.parts)), - AiTextMessage() => ('model', _convertParts(message.parts)), - ToolResponseMessage() => ('user', _convertParts(message.results)), - AiUiMessage() => ('model', _convertParts(message.parts)), - InternalMessage() => (null, []), // Not sent to model - }; - - if (role != null && parts.isNotEmpty) { - result.add(firebase_ai.Content(role, parts)); - } - } - return result; - } - - List _convertParts(List parts) { - final result = []; - for (final part in parts) { - switch (part) { - case TextPart(): - result.add(firebase_ai.TextPart(part.text)); - case DataPart(): - result.add( - firebase_ai.InlineDataPart( - 'application/json', - utf8.encode(jsonEncode(part.data)), - ), - ); - case ImagePart(): - if (part.bytes != null) { - result.add(firebase_ai.InlineDataPart(part.mimeType, part.bytes!)); - } else if (part.base64 != null) { - result.add( - firebase_ai.InlineDataPart( - part.mimeType, - base64.decode(part.base64!), - ), - ); - } else if (part.url != null) { - result.add(firebase_ai.TextPart('Image at ${part.url}')); - } else { - throw ContentConverterException('ImagePart has no data.'); - } - case ToolCallPart(): - result.add(firebase_ai.FunctionCall(part.toolName, part.arguments)); - case ToolResultPart(): - result.add( - firebase_ai.FunctionResponse( - part.callId, - // The result from ToolResultPart is a JSON string, but - // FunctionResponse expects a Map. - jsonDecode(part.result) as JsonMap, - ), - ); - case ThinkingPart(): - // Represent thoughts as text. - result.add(firebase_ai.TextPart('Thinking: ${part.text}')); - } - } - return result; - } -} diff --git a/packages/genui_firebase_ai/lib/src/gemini_generative_model.dart b/packages/genui_firebase_ai/lib/src/gemini_generative_model.dart deleted file mode 100644 index 1565d195b..000000000 --- a/packages/genui_firebase_ai/lib/src/gemini_generative_model.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:firebase_ai/firebase_ai.dart'; - -/// An interface for a generative model, allowing for mock implementations. -/// -/// This interface abstracts the underlying generative model, allowing for -/// different implementations to be used, for example, in testing. -abstract class GeminiGenerativeModelInterface { - /// Generates content from the given [content]. - Future generateContent(Iterable content); -} - -/// A wrapper for the `firebase_ai` [GenerativeModel] that implements the -/// [GeminiGenerativeModelInterface]. -/// -/// This class is used to wrap the `firebase_ai` [GenerativeModel] so that it -/// can be used interchangeably with other implementations of the -/// [GeminiGenerativeModelInterface]. -class GeminiGenerativeModel implements GeminiGenerativeModelInterface { - /// Creates a new [GeminiGenerativeModel] that wraps the given [_model]. - GeminiGenerativeModel(this._model); - - final GenerativeModel _model; - - @override - Future generateContent(Iterable content) { - return _model.generateContent(content); - } -} diff --git a/packages/genui_firebase_ai/lib/src/gemini_schema_adapter.dart b/packages/genui_firebase_ai/lib/src/gemini_schema_adapter.dart deleted file mode 100644 index 62a2ebe2d..000000000 --- a/packages/genui_firebase_ai/lib/src/gemini_schema_adapter.dart +++ /dev/null @@ -1,516 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:firebase_ai/firebase_ai.dart' as firebase_ai; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -/// An error that occurred during schema adaptation. -/// -/// This class encapsulates information about an error that occurred while -/// converting a `json_schema_builder` schema to a `firebase_ai` schema. -class GeminiSchemaAdapterError { - /// Creates an [GeminiSchemaAdapterError]. - /// - /// The [message] describes the error, and the [path] indicates where in the - /// schema the error occurred. - GeminiSchemaAdapterError(this.message, {required this.path}); - - /// A message describing the error. - final String message; - - /// The path to the location in the schema where the error occurred. - final List path; - - @override - String toString() => 'Error at path "${path.join('/')}": $message'; -} - -/// The result of a schema adaptation. -/// -/// This class holds the result of a schema conversion, including the adapted -/// schema and any errors that occurred during the process. -class GeminiSchemaAdapterResult { - /// Creates an [GeminiSchemaAdapterResult]. - /// - /// The [schema] is the result of the adaptation, and [errors] is a list of - /// any errors that were encountered. - GeminiSchemaAdapterResult(this.schema, this.errors); - - /// The adapted schema. - /// - /// This may be null if the schema could not be adapted at all. - final firebase_ai.Schema? schema; - - /// A list of errors that occurred during adaptation. - final List errors; -} - -/// An adapter to convert a [dsb.Schema] from the `json_schema_builder` package -/// to a [firebase_ai.Schema] from the `firebase_ai` package. -/// -/// This adapter attempts to convert as much of the schema as possible, -/// accumulating errors for any unsupported keywords or structures. The goal is -/// to produce a usable `firebase_ai` schema even if the source schema contains -/// features not supported by `firebase_ai`. -/// -/// Unsupported keywords will be ignored, and an [GeminiSchemaAdapterError] will -/// be added to the [GeminiSchemaAdapterResult.errors] list for each ignored -/// keyword. -class GeminiSchemaAdapter { - final List _errors = []; - - /// Adapts the given [schema] from `json_schema_builder` to `firebase_ai` - /// format. - /// - /// This is the main entry point for the adapter. It takes a [dsb.Schema] and - /// returns an [GeminiSchemaAdapterResult] containing the adapted - /// [firebase_ai.Schema] and a list of any errors that occurred. - GeminiSchemaAdapterResult adapt(dsb.Schema schema) { - _errors.clear(); - final firebase_ai.Schema? firebaseSchema = _adapt(schema, ['#']); - return GeminiSchemaAdapterResult( - firebaseSchema, - List.unmodifiable(_errors), - ); - } - - /// Recursively adapts a sub-schema. - /// - /// This method is called by [adapt] and recursively traverses the schema, - /// converting each part to the `firebase_ai` format. - firebase_ai.Schema? _adapt(dsb.Schema schema, List path) { - checkUnsupportedGlobalKeywords(schema, path); - - if (schema.value.containsKey('anyOf')) { - final Object? anyOfList = schema.value['anyOf']; - if (anyOfList is List && anyOfList.isNotEmpty) { - final schemas = []; - for (var i = 0; i < anyOfList.length; i++) { - final Object? subSchemaMap = anyOfList[i]; - if (subSchemaMap is! Map) { - _errors.add( - GeminiSchemaAdapterError( - 'Schema inside "anyOf" must be an object.', - path: [...path, 'anyOf', i.toString()], - ), - ); - continue; - } - final subSchema = dsb.Schema.fromMap(subSchemaMap); - final subPath = [...path, 'anyOf', i.toString()]; - final firebase_ai.Schema? adaptedSchema = _adapt(subSchema, subPath); - if (adaptedSchema != null) { - schemas.add(adaptedSchema); - } - } - if (schemas.isNotEmpty) { - return firebase_ai.Schema.anyOf(schemas: schemas); - } - } else { - _errors.add( - GeminiSchemaAdapterError( - 'The value of "anyOf" must be a non-empty array of schemas.', - path: path, - ), - ); - } - } - - final Object? type = schema.type; - String? typeName; - if (type is String) { - typeName = type; - } else if (type is List) { - if (type.isEmpty) { - _errors.add( - GeminiSchemaAdapterError( - 'Schema has an empty "type" array.', - path: path, - ), - ); - return null; - } - typeName = type.first as String; - if (type.length > 1) { - _errors.add( - GeminiSchemaAdapterError( - 'Multiple types found (${type.join(', ')}). Only the first type ' - '"$typeName" will be used.', - path: path, - ), - ); - } - } else if (dsb.ObjectSchema.fromMap(schema.value).properties != null || - schema.value.containsKey('properties')) { - typeName = dsb.JsonType.object.typeName; - } else if (schema.value.containsKey('items')) { - typeName = dsb.JsonType.list.typeName; - } - - if (typeName == null) { - _errors.add( - GeminiSchemaAdapterError( - 'Schema must have a "type" or be implicitly typed with "properties" ' - 'or "items".', - path: path, - ), - ); - return null; - } - - switch (typeName) { - case 'object': - return _adaptObject(schema, path); - case 'array': - return _adaptArray(schema, path); - case 'string': - return _adaptString(schema, path); - case 'number': - return _adaptNumber(schema, path); - case 'integer': - return _adaptInteger(schema, path); - case 'boolean': - return _adaptBoolean(schema, path); - case 'null': - return _adaptNull(schema, path); - default: - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported schema type "$typeName".', - path: path, - ), - ); - return null; - } - } - - /// Checks for and logs errors for unsupported global keywords. - void checkUnsupportedGlobalKeywords(dsb.Schema schema, List path) { - const unsupportedKeywords = { - '\$comment', - 'default', - 'examples', - 'deprecated', - 'readOnly', - 'writeOnly', - '\$defs', - '\$ref', - '\$anchor', - '\$dynamicAnchor', - '\$id', - '\$schema', - 'allOf', - 'oneOf', - 'not', - 'if', - 'then', - 'else', - 'dependentSchemas', - 'const', - }; - - for (final keyword in unsupportedKeywords) { - if (schema.value.containsKey(keyword)) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "$keyword". It will be ignored.', - path: path, - ), - ); - } - } - } - - /// Adapts an object schema. - firebase_ai.Schema? _adaptObject(dsb.Schema dsbSchema, List path) { - final objectSchema = dsb.ObjectSchema.fromMap(dsbSchema.value); - final properties = {}; - if (objectSchema.properties != null) { - for (final MapEntry entry - in objectSchema.properties!.entries) { - final List propertyPath = [...path, 'properties', entry.key]; - final firebase_ai.Schema? adaptedProperty = _adapt( - entry.value, - propertyPath, - ); - if (adaptedProperty != null) { - properties[entry.key] = adaptedProperty; - } - } - } - - if (objectSchema.patternProperties != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "patternProperties". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.dependentRequired != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "dependentRequired". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.additionalProperties != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "additionalProperties". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.unevaluatedProperties != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "unevaluatedProperties". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.propertyNames != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "propertyNames". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.minProperties != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "minProperties". It will be ignored.', - path: path, - ), - ); - } - if (objectSchema.maxProperties != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "maxProperties". It will be ignored.', - path: path, - ), - ); - } - - final Set allProperties = properties.keys.toSet(); - final Set requiredProperties = objectSchema.required?.toSet() ?? {}; - final List optionalProperties = allProperties - .difference(requiredProperties) - .toList(); - - return firebase_ai.Schema( - firebase_ai.SchemaType.object, - properties: properties, - optionalProperties: optionalProperties, - description: dsbSchema.description, - ); - } - - /// Adapts an array schema. - firebase_ai.Schema? _adaptArray(dsb.Schema dsbSchema, List path) { - final listSchema = dsb.ListSchema.fromMap(dsbSchema.value); - - if (listSchema.items == null) { - _errors.add( - GeminiSchemaAdapterError( - 'Array schema must have an "items" property.', - path: path, - ), - ); - return null; - } - - final itemsPath = [...path, 'items']; - final firebase_ai.Schema? adaptedItems = _adapt( - listSchema.items!, - itemsPath, - ); - if (adaptedItems == null) { - return null; - } - - if (listSchema.prefixItems != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "prefixItems". It will be ignored.', - path: path, - ), - ); - } - if (listSchema.unevaluatedItems != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "unevaluatedItems". It will be ignored.', - path: path, - ), - ); - } - if (listSchema.contains != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "contains". It will be ignored.', - path: path, - ), - ); - } - if (listSchema.minContains != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "minContains". It will be ignored.', - path: path, - ), - ); - } - if (listSchema.maxContains != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "maxContains". It will be ignored.', - path: path, - ), - ); - } - if (listSchema.uniqueItems ?? false) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "uniqueItems". It will be ignored.', - path: path, - ), - ); - } - - return firebase_ai.Schema( - firebase_ai.SchemaType.array, - items: adaptedItems, - minItems: listSchema.minItems, - maxItems: listSchema.maxItems, - description: dsbSchema.description, - ); - } - - /// Adapts a string schema. - firebase_ai.Schema? _adaptString(dsb.Schema dsbSchema, List path) { - final stringSchema = dsb.StringSchema.fromMap(dsbSchema.value); - if (stringSchema.minLength != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "minLength". It will be ignored.', - path: path, - ), - ); - } - if (stringSchema.maxLength != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "maxLength". It will be ignored.', - path: path, - ), - ); - } - if (stringSchema.pattern != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "pattern". It will be ignored.', - path: path, - ), - ); - } - return firebase_ai.Schema( - firebase_ai.SchemaType.string, - format: stringSchema.format, - enumValues: stringSchema.enumValues?.map((e) => e.toString()).toList(), - description: dsbSchema.description, - ); - } - - /// Adapts a number schema. - firebase_ai.Schema? _adaptNumber(dsb.Schema dsbSchema, List path) { - final numberSchema = dsb.NumberSchema.fromMap(dsbSchema.value); - if (numberSchema.exclusiveMinimum != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "exclusiveMinimum". It will be ignored.', - path: path, - ), - ); - } - if (numberSchema.exclusiveMaximum != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "exclusiveMaximum". It will be ignored.', - path: path, - ), - ); - } - if (numberSchema.multipleOf != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "multipleOf". It will be ignored.', - path: path, - ), - ); - } - return firebase_ai.Schema( - firebase_ai.SchemaType.number, - minimum: numberSchema.minimum?.toDouble(), - maximum: numberSchema.maximum?.toDouble(), - description: dsbSchema.description, - ); - } - - /// Adapts an integer schema. - firebase_ai.Schema? _adaptInteger(dsb.Schema dsbSchema, List path) { - final integerSchema = dsb.IntegerSchema.fromMap(dsbSchema.value); - if (integerSchema.exclusiveMinimum != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "exclusiveMinimum". It will be ignored.', - path: path, - ), - ); - } - if (integerSchema.exclusiveMaximum != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "exclusiveMaximum". It will be ignored.', - path: path, - ), - ); - } - if (integerSchema.multipleOf != null) { - _errors.add( - GeminiSchemaAdapterError( - 'Unsupported keyword "multipleOf". It will be ignored.', - path: path, - ), - ); - } - return firebase_ai.Schema( - firebase_ai.SchemaType.integer, - minimum: integerSchema.minimum?.toDouble(), - maximum: integerSchema.maximum?.toDouble(), - description: dsbSchema.description, - ); - } - - /// Adapts a boolean schema. - firebase_ai.Schema? _adaptBoolean(dsb.Schema dsbSchema, List path) { - return firebase_ai.Schema( - firebase_ai.SchemaType.boolean, - description: dsbSchema.description, - ); - } - - /// Adapts a null schema. - firebase_ai.Schema? _adaptNull(dsb.Schema dsbSchema, List path) { - return firebase_ai.Schema( - firebase_ai.SchemaType.object, - nullable: true, - description: dsbSchema.description, - ); - } -} diff --git a/packages/genui_firebase_ai/pubspec.yaml b/packages/genui_firebase_ai/pubspec.yaml deleted file mode 100644 index e979dc9bb..000000000 --- a/packages/genui_firebase_ai/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: genui_firebase_ai -description: Integration package for genui and Firebase AI Logic. -version: 0.7.0 -homepage: https://github.com/flutter/genui/tree/main/packages/genui_firebase_ai -license: BSD-3-Clause -issue_tracker: https://github.com/flutter/genui/issues - -environment: - sdk: ">=3.9.2 <4.0.0" - flutter: ">=3.35.7 <4.0.0" - -resolution: workspace - -dependencies: - firebase_ai: ^3.5.0 - flutter: - sdk: flutter - genui: ^0.7.0 - json_schema_builder: ^0.1.3 - -dev_dependencies: - flutter_test: - sdk: flutter - logging: ^1.3.0 diff --git a/packages/genui_firebase_ai/test/firebase_ai_content_generator_test.dart b/packages/genui_firebase_ai/test/firebase_ai_content_generator_test.dart deleted file mode 100644 index d82b50aaa..000000000 --- a/packages/genui_firebase_ai/test/firebase_ai_content_generator_test.dart +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:firebase_ai/firebase_ai.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart' as genui; -import 'package:genui_firebase_ai/src/firebase_ai_content_generator.dart'; -import 'package:genui_firebase_ai/src/gemini_generative_model.dart'; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -void main() { - group('FirebaseAiContentGenerator', () { - test('isProcessing is true during request', () async { - final generator = FirebaseAiContentGenerator( - catalog: const genui.Catalog({}), - modelCreator: - ({required configuration, systemInstruction, tools, toolConfig}) { - return FakeGeminiGenerativeModel([ - GenerateContentResponse([ - Candidate( - Content.model([ - const FunctionCall('provideFinalOutput', { - 'output': {'response': 'Hello'}, - }), - ]), - [], - null, - FinishReason.stop, - '', - ), - ], null), - ]); - }, - ); - - expect(generator.isProcessing.value, isFalse); - final Future future = generator.sendRequest( - genui.UserMessage([const genui.TextPart('Hi')]), - ); - expect(generator.isProcessing.value, isTrue); - await future; - expect(generator.isProcessing.value, isFalse); - }); - - test('can call a tool and return a result', () async { - final generator = FirebaseAiContentGenerator( - catalog: const genui.Catalog({}), - additionalTools: [ - genui.DynamicAiTool>( - name: 'testTool', - description: 'A test tool', - parameters: dsb.Schema.object(), - invokeFunction: (args) async => {'result': 'tool result'}, - ), - ], - modelCreator: - ({required configuration, systemInstruction, tools, toolConfig}) { - return FakeGeminiGenerativeModel([ - GenerateContentResponse([ - Candidate( - Content.model([const FunctionCall('testTool', {})]), - [], - null, - FinishReason.stop, - '', - ), - ], null), - GenerateContentResponse([ - Candidate( - Content.model([ - const FunctionCall('provideFinalOutput', { - 'output': {'response': 'Tool called'}, - }), - ]), - [], - null, - FinishReason.stop, - '', - ), - ], null), - ]); - }, - ); - - final hi = genui.UserMessage([const genui.TextPart('Hi')]); - final completer = Completer(); - unawaited(generator.textResponseStream.first.then(completer.complete)); - await generator.sendRequest(hi); - final String response = await completer.future; - expect(response, 'Tool called'); - }); - - test('returns a simple text response', () async { - final generator = FirebaseAiContentGenerator( - catalog: const genui.Catalog({}), - modelCreator: - ({required configuration, systemInstruction, tools, toolConfig}) { - return FakeGeminiGenerativeModel([ - GenerateContentResponse([ - Candidate( - Content.model([ - const FunctionCall('provideFinalOutput', { - 'output': {'response': 'Hello'}, - }), - ]), - [], - null, - FinishReason.stop, - '', - ), - ], null), - ]); - }, - ); - - final hi = genui.UserMessage([const genui.TextPart('Hi')]); - final completer = Completer(); - unawaited(generator.textResponseStream.first.then(completer.complete)); - await generator.sendRequest(hi); - final String response = await completer.future; - expect(response, 'Hello'); - }); - }); -} - -class FakeGeminiGenerativeModel implements GeminiGenerativeModelInterface { - FakeGeminiGenerativeModel(this.responses); - - final List responses; - int callCount = 0; - - @override - Future generateContent(Iterable content) { - return Future.delayed(Duration.zero, () => responses[callCount++]); - } -} diff --git a/packages/genui_firebase_ai/test/gemini_content_converter_test.dart b/packages/genui_firebase_ai/test/gemini_content_converter_test.dart deleted file mode 100644 index 1314a20ac..000000000 --- a/packages/genui_firebase_ai/test/gemini_content_converter_test.dart +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:firebase_ai/firebase_ai.dart' as firebase_ai; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart'; - -void main() { - group('GeminiContentConverter', () { - late GeminiContentConverter converter; - - setUp(() { - converter = GeminiContentConverter(); - }); - - test('toFirebaseAiContent converts $UserMessage with $TextPart', () { - final messages = [UserMessage.text('Hello')]; - final List result = converter.toFirebaseAiContent( - messages, - ); - - expect(result, hasLength(1)); - expect(result.first.role, 'user'); - expect(result.first.parts, hasLength(1)); - expect(result.first.parts.first, isA()); - expect((result.first.parts.first as firebase_ai.TextPart).text, 'Hello'); - }); - - test('toFirebaseAiContent converts $AiTextMessage with $TextPart', () { - final messages = [AiTextMessage.text('Hi there')]; - final List result = converter.toFirebaseAiContent( - messages, - ); - - expect(result, hasLength(1)); - expect(result.first.role, 'model'); - expect(result.first.parts, hasLength(1)); - expect(result.first.parts.first, isA()); - expect( - (result.first.parts.first as firebase_ai.TextPart).text, - 'Hi there', - ); - }); - - test('toFirebaseAiContent converts $AiUiMessage', () { - final definition = UiDefinition(surfaceId: 'testSurface'); - final messages = [AiUiMessage(definition: definition)]; - final List result = converter.toFirebaseAiContent( - messages, - ); - expect(result, hasLength(1)); - expect(result.first.role, 'model'); - expect(result.first.parts, hasLength(1)); - expect(result.first.parts.first, isA()); - expect( - (result.first.parts.first as firebase_ai.TextPart).text, - definition.asContextDescriptionText(), - ); - }); - - test('toFirebaseAiContent ignores $InternalMessage', () { - final messages = [const InternalMessage('Thinking...')]; - final List result = converter.toFirebaseAiContent( - messages, - ); - expect(result, isEmpty); - }); - - test('toFirebaseAiContent converts multi-part $UserMessage', () { - final messages = [ - UserMessage([ - const TextPart('Look at this image'), - ImagePart.fromBytes(Uint8List(0), mimeType: 'image/png'), - ]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - - expect(result, hasLength(1)); - expect(result.first.role, 'user'); - expect(result.first.parts, hasLength(2)); - expect(result.first.parts[0], isA()); - expect(result.first.parts[1], isA()); - }); - - test('toFirebaseAiContent converts $DataPart', () { - final data = {'key': 'value'}; - final messages = [ - UserMessage([DataPart(data)]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.InlineDataPart; - expect(part.mimeType, 'application/json'); - expect(utf8.decode(part.bytes), jsonEncode(data)); - }); - - test('toFirebaseAiContent converts $ImagePart from bytes', () { - final bytes = Uint8List.fromList([1, 2, 3]); - final messages = [ - UserMessage([ImagePart.fromBytes(bytes, mimeType: 'image/jpeg')]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.InlineDataPart; - expect(part.mimeType, 'image/jpeg'); - expect(part.bytes, bytes); - }); - - test('toFirebaseAiContent converts $ImagePart from base64', () { - const base64String = 'AQID'; // base64 for [1, 2, 3] - final messages = [ - UserMessage([ - const ImagePart.fromBase64(base64String, mimeType: 'image/png'), - ]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.InlineDataPart; - expect(part.mimeType, 'image/png'); - expect(part.bytes, base64.decode(base64String)); - }); - - test('toFirebaseAiContent converts $ImagePart from URL', () { - final Uri url = Uri.parse('http://example.com/image.jpg'); - final messages = [ - UserMessage([ImagePart.fromUrl(url, mimeType: 'image/jpeg')]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.TextPart; - expect(part.text, 'Image at $url'); - }); - - test('toFirebaseAiContent converts $ToolCallPart', () { - final messages = [ - AiTextMessage([ - const ToolCallPart( - id: 'call1', - toolName: 'doSomething', - arguments: {'arg': 'value'}, - ), - ]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.FunctionCall; - expect(part.name, 'doSomething'); - expect(part.args, {'arg': 'value'}); - }); - - test('toFirebaseAiContent converts $ToolResponseMessage', () { - final messages = [ - ToolResponseMessage([ - ToolResultPart(callId: 'call1', result: jsonEncode({'data': 'ok'})), - ]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - expect(result.first.role, 'user'); - final part = result.first.parts.first as firebase_ai.FunctionResponse; - expect(part.name, 'call1'); - expect(part.response, {'data': 'ok'}); - }); - - test('toFirebaseAiContent converts $ThinkingPart', () { - final messages = [ - AiTextMessage([const ThinkingPart('working on it')]), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - final part = result.first.parts.first as firebase_ai.TextPart; - expect(part.text, 'Thinking: working on it'); - }); - - test( - 'toFirebaseAiContent handles multiple messages of different types', - () { - final List messages = [ - UserMessage.text('First message'), - AiTextMessage.text('Second message'), - UserMessage.text('Third message'), - ]; - final List result = converter.toFirebaseAiContent( - messages, - ); - - expect(result, hasLength(3)); - expect(result[0].role, 'user'); - expect( - (result[0].parts.first as firebase_ai.TextPart).text, - 'First message', - ); - expect(result[1].role, 'model'); - expect( - (result[1].parts.first as firebase_ai.TextPart).text, - 'Second message', - ); - expect(result[2].role, 'user'); - expect( - (result[2].parts.first as firebase_ai.TextPart).text, - 'Third message', - ); - }, - ); - }); -} diff --git a/packages/genui_firebase_ai/test/gemini_schema_adapter_test.dart b/packages/genui_firebase_ai/test/gemini_schema_adapter_test.dart deleted file mode 100644 index 9ef488c43..000000000 --- a/packages/genui_firebase_ai/test/gemini_schema_adapter_test.dart +++ /dev/null @@ -1,527 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:firebase_ai/firebase_ai.dart' as firebase_ai; -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui_firebase_ai/genui_firebase_ai.dart'; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -void main() { - group('GeminiSchemaAdapter', () { - late GeminiSchemaAdapter adapter; - - setUp(() { - adapter = GeminiSchemaAdapter(); - }); - - group('adaptObject', () { - test('should adapt a simple object schema', () { - final dsbSchema = dsb.Schema.object( - properties: { - 'name': dsb.Schema.string(description: 'The name of the person.'), - 'age': dsb.Schema.integer(description: 'The age of the person.'), - }, - required: ['name'], - description: 'A person object.', - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.object); - expect(result.schema!.description, 'A person object.'); - expect(result.schema!.properties, hasLength(2)); - expect( - result.schema!.properties!['name']!.type, - firebase_ai.SchemaType.string, - ); - expect( - result.schema!.properties!['name']!.description, - 'The name of the person.', - ); - expect( - result.schema!.properties!['age']!.type, - firebase_ai.SchemaType.integer, - ); - expect( - result.schema!.properties!['age']!.description, - 'The age of the person.', - ); - expect(result.schema!.optionalProperties, equals(['age'])); - }); - - test('should handle unsupported keywords and log errors', () { - final dsbSchema = dsb.Schema.object( - properties: {'name': dsb.Schema.string()}, - minProperties: 1, - maxProperties: 5, - additionalProperties: dsb.Schema.boolean(), - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(3)); - expect( - result.errors[0].message, - contains('Unsupported keyword "additionalProperties"'), - ); - expect( - result.errors[1].message, - contains('Unsupported keyword "minProperties"'), - ); - expect( - result.errors[2].message, - contains('Unsupported keyword "maxProperties"'), - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.object); - }); - }); - - group('adaptArray', () { - test('should adapt a simple array schema', () { - final dsbSchema = dsb.Schema.list( - items: dsb.Schema.string(), - minItems: 1, - maxItems: 10, - description: 'A list of items.', - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.array); - expect(result.schema!.description, 'A list of items.'); - expect(result.schema!.items, isNotNull); - expect(result.schema!.items!.type, firebase_ai.SchemaType.string); - expect(result.schema!.minItems, 1); - expect(result.schema!.maxItems, 10); - }); - - test('should log an error if items is missing', () { - final dsbSchema = dsb.Schema.fromMap({'type': 'array'}); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isNotEmpty); - expect( - result.errors.first.message, - 'Array schema must have an "items" property.', - ); - expect(result.schema, isNull); - }); - - test('should handle unsupported keywords and log errors', () { - final dsbSchema = dsb.Schema.list( - items: dsb.Schema.string(), - uniqueItems: true, - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(1)); - expect( - result.errors[0].message, - contains('Unsupported keyword "uniqueItems"'), - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.array); - }); - }); - - group('adaptString', () { - test('should adapt a simple string schema', () { - final dsbSchema = dsb.Schema.string( - format: 'email', - enumValues: ['test@example.com', 'user@example.com'], - description: 'A choice of fruit.', - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.string); - expect(result.schema!.description, 'A choice of fruit.'); - expect(result.schema!.format, 'email'); - expect(result.schema!.enumValues, [ - 'test@example.com', - 'user@example.com', - ]); - }); - - test('should handle unsupported keywords and log errors', () { - final dsbSchema = dsb.Schema.string( - minLength: 1, - maxLength: 10, - pattern: r'^[a-zA-Z]+$', - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(3)); - expect( - result.errors[0].message, - contains('Unsupported keyword "minLength"'), - ); - expect( - result.errors[1].message, - contains('Unsupported keyword "maxLength"'), - ); - expect( - result.errors[2].message, - contains('Unsupported keyword "pattern"'), - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.string); - }); - }); - - group('adaptNumber', () { - test('should adapt a simple number schema', () { - final dsbSchema = dsb.Schema.number(minimum: 0.0, maximum: 100.0); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.number); - expect(result.schema!.minimum, 0.0); - expect(result.schema!.maximum, 100.0); - }); - - test('should handle unsupported keywords and log errors', () { - final dsbSchema = dsb.Schema.number( - exclusiveMinimum: 0.0, - exclusiveMaximum: 100.0, - multipleOf: 5.0, - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(3)); - expect( - result.errors[0].message, - contains('Unsupported keyword "exclusiveMinimum"'), - ); - expect( - result.errors[1].message, - contains('Unsupported keyword "exclusiveMaximum"'), - ); - expect( - result.errors[2].message, - contains('Unsupported keyword "multipleOf"'), - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.number); - }); - }); - - group('adaptInteger', () { - test('should adapt a simple integer schema', () { - final dsbSchema = dsb.Schema.integer(minimum: 0, maximum: 100); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.integer); - expect(result.schema!.minimum, 0.0); - expect(result.schema!.maximum, 100.0); - }); - - test('should handle unsupported keywords and log errors', () { - final dsbSchema = dsb.Schema.integer( - exclusiveMinimum: 0, - exclusiveMaximum: 100, - multipleOf: 5, - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(3)); - expect( - result.errors[0].message, - contains('Unsupported keyword "exclusiveMinimum"'), - ); - expect( - result.errors[1].message, - contains('Unsupported keyword "exclusiveMaximum"'), - ); - expect( - result.errors[2].message, - contains('Unsupported keyword "multipleOf"'), - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.integer); - }); - }); - - group('adaptBoolean', () { - test('should adapt a boolean schema', () { - final dsbSchema = dsb.Schema.boolean(); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.boolean); - }); - }); - - group('adaptNull', () { - test('should adapt a null schema to a nullable object', () { - final dsbSchema = dsb.Schema.nil(); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.object); - expect(result.schema!.nullable, isTrue); - }); - }); - - group('General Error Handling', () { - test('should log an error for an unknown type', () { - final dsbSchema = dsb.Schema.fromMap({'type': 'unknown'}); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isNotEmpty); - expect( - result.errors.first.message, - 'Unsupported schema type "unknown".', - ); - expect(result.schema, isNull); - }); - - test('should log an error for a schema with no type', () { - final dsbSchema = dsb.Schema.fromMap({}); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isNotEmpty); - expect( - result.errors.first.message, - 'Schema must have a "type" or be implicitly typed with ' - '"properties" or "items".', - ); - expect(result.schema, isNull); - }); - - test('should handle multiple types and use the first one', () { - final dsbSchema = dsb.Schema.fromMap({ - 'type': ['string', 'integer'], - }); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, hasLength(1)); - expect( - result.errors.first.message, - 'Multiple types found (string, integer). Only the first type ' - '"string" will be used.', - ); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.string); - }); - - test('should handle an empty type array', () { - final dsbSchema = dsb.Schema.fromMap({'type': []}); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, hasLength(1)); - expect( - result.errors.first.message, - 'Schema has an empty "type" array.', - ); - expect(result.schema, isNull); - }); - }); - - group('anyOf', () { - test('should adapt a schema with anyOf', () { - final dsbSchema = dsb.Schema.combined( - anyOf: [ - { - 'properties': { - 'bar': {'type': 'number'}, - }, - }, - { - 'properties': { - 'baz': {'type': 'boolean'}, - }, - }, - ], - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.anyOf); - expect(result.schema!.properties, isNull); - expect(result.schema!.anyOf, isNotNull); - expect(result.schema!.anyOf, hasLength(2)); - final firebase_ai.Schema firstAnyOf = result.schema!.anyOf![0]; - expect(firstAnyOf.type, firebase_ai.SchemaType.object); - expect(firstAnyOf.properties, hasLength(1)); - expect( - firstAnyOf.properties!['bar']!.type, - firebase_ai.SchemaType.number, - ); - final firebase_ai.Schema secondAnyOf = result.schema!.anyOf![1]; - expect(secondAnyOf.type, firebase_ai.SchemaType.object); - expect(secondAnyOf.properties, hasLength(1)); - expect( - secondAnyOf.properties!['baz']!.type, - firebase_ai.SchemaType.boolean, - ); - }); - - test('should report an error for an empty anyOf list', () { - final dsbSchema = dsb.Schema.fromMap({'type': 'object', 'anyOf': []}); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(1)); - expect( - result.errors[0].message, - 'The value of "anyOf" must be a non-empty array of schemas.', - ); - }); - - test('should report an error for a non-list anyOf', () { - final dsbSchema = dsb.Schema.fromMap({ - 'type': 'object', - 'anyOf': 'not-a-list', - }); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(1)); - expect( - result.errors[0].message, - 'The value of "anyOf" must be a non-empty array of schemas.', - ); - }); - - test('should report an error for invalid item in anyOf list', () { - final dsbSchema = dsb.Schema.fromMap({ - 'type': 'object', - 'anyOf': ['not-a-schema'], - }); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, hasLength(1)); - expect( - result.errors[0].message, - 'Schema inside "anyOf" must be an object.', - ); - }); - }); - - group('Edge Cases', () { - test('should handle nested objects and arrays', () { - final dsbSchema = dsb.Schema.object( - properties: { - 'user': dsb.Schema.object( - properties: { - 'name': dsb.Schema.string(), - 'roles': dsb.Schema.list(items: dsb.Schema.string()), - }, - required: ['name'], - ), - }, - ); - - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - final firebase_ai.Schema userSchema = - result.schema!.properties!['user']!; - expect(userSchema.type, firebase_ai.SchemaType.object); - expect(userSchema.properties, hasLength(2)); - expect(userSchema.optionalProperties, equals(['roles'])); - final firebase_ai.Schema rolesSchema = userSchema.properties!['roles']!; - expect(rolesSchema.type, firebase_ai.SchemaType.array); - expect(rolesSchema.items!.type, firebase_ai.SchemaType.string); - }); - - test('should handle implicitly typed object schema', () { - final dsbSchema = dsb.Schema.fromMap({ - 'properties': { - 'name': {'type': 'string'}, - }, - }); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.object); - }); - - test('should handle implicitly typed array schema', () { - final dsbSchema = dsb.Schema.fromMap({ - 'items': {'type': 'string'}, - }); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.array); - }); - - test('should handle an empty object schema', () { - final dsbSchema = dsb.Schema.object(properties: {}); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.object); - expect(result.schema!.properties, isEmpty); - expect(result.schema!.optionalProperties, isEmpty); - }); - - test('should handle an object with all properties required', () { - final dsbSchema = dsb.Schema.object( - properties: { - 'name': dsb.Schema.string(), - 'age': dsb.Schema.integer(), - }, - required: ['name', 'age'], - ); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.optionalProperties, isEmpty); - }); - - test('should handle an object with no required properties', () { - final dsbSchema = dsb.Schema.object( - properties: { - 'name': dsb.Schema.string(), - 'age': dsb.Schema.integer(), - }, - ); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect( - result.schema!.optionalProperties, - unorderedEquals(['name', 'age']), - ); - }); - - test('should handle an array of objects', () { - final dsbSchema = dsb.Schema.list( - items: dsb.Schema.object( - properties: { - 'name': dsb.Schema.string(), - 'value': dsb.Schema.integer(), - }, - required: ['name'], - ), - ); - final GeminiSchemaAdapterResult result = adapter.adapt(dsbSchema); - expect(result.errors, isEmpty); - expect(result.schema, isNotNull); - expect(result.schema!.type, firebase_ai.SchemaType.array); - final firebase_ai.Schema itemsSchema = result.schema!.items!; - expect(itemsSchema.type, firebase_ai.SchemaType.object); - expect(itemsSchema.properties, hasLength(2)); - expect(itemsSchema.optionalProperties, equals(['value'])); - }); - }); - }); -} diff --git a/packages/genui_firebase_ai/test/test_infra/utils.dart b/packages/genui_firebase_ai/test/test_infra/utils.dart deleted file mode 100644 index 219537815..000000000 --- a/packages/genui_firebase_ai/test/test_infra/utils.dart +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:firebase_ai/firebase_ai.dart'; -import 'package:genui_firebase_ai/src/gemini_generative_model.dart'; - -// A fake GenerativeModel that doesn't extend or implement the real one, -// to work around the final class restriction. -class FakeGenerativeModel implements GeminiGenerativeModelInterface { - int generateContentCallCount = 0; - GenerateContentResponse? response; - List responses = []; - Exception? exception; - PromptFeedback? promptFeedback; - - @override - Future generateContent( - Iterable content, - ) async { - generateContentCallCount++; - if (exception != null) { - final Exception? e = exception; - exception = null; // Reset for next call - throw e!; - } - if (responses.isNotEmpty) { - final GenerateContentResponse response = responses.removeAt(0); - return GenerateContentResponse(response.candidates, promptFeedback); - } - if (response != null) { - return GenerateContentResponse(response!.candidates, promptFeedback); - } - throw StateError( - 'No response or exception configured for FakeGenerativeModel', - ); - } -} diff --git a/packages/genui_google_generative_ai/CHANGELOG.md b/packages/genui_google_generative_ai/CHANGELOG.md deleted file mode 100644 index 719137971..000000000 --- a/packages/genui_google_generative_ai/CHANGELOG.md +++ /dev/null @@ -1,40 +0,0 @@ -# `genui_google_generative_ai` Changelog - -## 0.7.1 (in progress) - -## 0.7.0 - -- Updated version to match `genui` package version. - -## 0.6.1 - -- **Fix**: Ensure bytes are not null when creating Blob in content converter. - -## 0.6.0 - -- **BREAKING**: Removed `GenUiConfiguration` from `GoogleGenerativeAiContentGenerator`. -- Bumped dependencies: `google_cloud_ai_generativelanguage_v1beta` to ^0.3.0 and `google_cloud_protobuf` to ^0.3.0. - -- Bump dependent package versions to the latest that work with Flutter stable. - -## 0.5.1 - -- Homepage URL was updated. - -## 0.5.0 - -- Initial release of `genui_google_generative_ai` -- Implements `ContentGenerator` using Google Cloud Generative Language API -- Provides `GoogleGenerativeAiContentGenerator` with support for: - - Tool calling and function declarations - - Schema adaptation from `json_schema_builder` to Google Cloud API format - - Message conversion between genui ChatMessages and Google Cloud Content - - Protocol Buffer Struct type conversion for function calling - - System instructions and custom model selection - - Token usage tracking -- Includes `GoogleContentConverter` for message format conversion -- Includes `GoogleSchemaAdapter` for JSON schema adaptation -- Compatible with Google Cloud Generative Language API v1beta -- Supports multiple Type enum values (string, number, integer, boolean, array, object) -- Handles `FunctionCallingConfig` with AUTO and ANY modes - diff --git a/packages/genui_google_generative_ai/LICENSE b/packages/genui_google_generative_ai/LICENSE deleted file mode 100644 index ed9448d9f..000000000 --- a/packages/genui_google_generative_ai/LICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright 2025 The Flutter Authors. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/packages/genui_google_generative_ai/README.md b/packages/genui_google_generative_ai/README.md deleted file mode 100644 index e02d284c7..000000000 --- a/packages/genui_google_generative_ai/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# genui_google_generative_ai - -This package provides the integration between `genui` and the Google Cloud Generative Language API. It allows you to use the power of Google's Gemini models to generate dynamic user interfaces in your Flutter applications. - -## Features - -- **GoogleGenerativeAiContentGenerator:** An implementation of `ContentGenerator` that connects to the Google Cloud Generative Language API. -- **GoogleContentConverter:** Converts between the generic `ChatMessage` and the `google_cloud_ai_generativelanguage_v1beta` specific `Content` classes. -- **GoogleSchemaAdapter:** Adapts schemas from `json_schema_builder` to the Google Cloud API format. - -## Getting Started - -To use this package, you will need a Gemini API key. If you don't already have one, you can get it for free in [Google AI Studio](https://aistudio.google.com/apikey). - -### Installation - -Use `flutter pub add` to add the latest versions of `genui` and `genui_google_generative_ai` as -dependencies in your `pubspec.yaml` file: - -```bash -flutter pub add genui genui_google_generative_ai -``` - -### Usage - -Create an instance of `GoogleGenerativeAiContentGenerator` and pass it to your `GenUiConversation`: - -```dart -import 'package:genui/genui.dart'; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; - -final catalog = Catalog(components: [...]); - -final contentGenerator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - systemInstruction: 'You are a helpful assistant.', - modelName: 'models/gemini-2.5-flash', - apiKey: 'YOUR_API_KEY', // Or set GEMINI_API_KEY environment variable -); - -final conversation = GenUiConversation( - contentGenerator: contentGenerator, -); -``` - -### API Key Configuration - -The API key can be provided in two ways: - -1. **Environment Variable** (recommended): Set the `GEMINI_API_KEY` environment variable -2. **Constructor Parameter**: Pass the API key directly to the constructor - -If neither is provided, the package will attempt to use the default environment variable. - -## Differences from Firebase AI - -This package uses the Google Cloud Generative Language API instead of Firebase AI Logic. - -This API is meant for quick explorations and local testing or prototyping, -not for production or deployment. - -**Flutter apps built for production should use Firebase AI**: For mobile and -web applications, consider using `genui_firebase_ai` instead, which provides client-side access - -## Documentation - -For more information on the Google Cloud Generative Language API, see: -- [API Documentation](https://pub.dev/documentation/google_cloud_ai_generativelanguage_v1beta/latest/) -- [Gemini API Guide](https://ai.google.dev/gemini-api/docs) - -## License - -This package is licensed under the BSD-3-Clause license. See [LICENSE](LICENSE) for details. diff --git a/packages/genui_google_generative_ai/analysis_options.yaml b/packages/genui_google_generative_ai/analysis_options.yaml deleted file mode 100644 index 4f059e0f4..000000000 --- a/packages/genui_google_generative_ai/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -include: package:dart_flutter_team_lints/analysis_options.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options - diff --git a/packages/genui_google_generative_ai/lib/genui_google_generative_ai.dart b/packages/genui_google_generative_ai/lib/genui_google_generative_ai.dart deleted file mode 100644 index 2fafccc20..000000000 --- a/packages/genui_google_generative_ai/lib/genui_google_generative_ai.dart +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/google_content_converter.dart'; -export 'src/google_generative_ai_content_generator.dart'; -export 'src/google_generative_service_interface.dart'; -export 'src/google_schema_adapter.dart'; diff --git a/packages/genui_google_generative_ai/lib/src/google_content_converter.dart b/packages/genui_google_generative_ai/lib/src/google_content_converter.dart deleted file mode 100644 index c0f962a01..000000000 --- a/packages/genui_google_generative_ai/lib/src/google_content_converter.dart +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:genui/genui.dart'; -import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' - as google_ai; -import 'package:google_cloud_protobuf/protobuf.dart' as protobuf; - -/// An exception thrown by this package. -class GoogleAiClientException implements Exception { - /// Creates a [GoogleAiClientException] with the given [message]. - GoogleAiClientException(this.message); - - /// The message associated with the exception. - final String message; - - @override - String toString() => '$GoogleAiClientException: $message'; -} - -/// A class to convert between the generic `ChatMessage` and the `google_ai` -/// specific `Content` classes. -/// -/// This class is responsible for translating the abstract [ChatMessage] -/// representation into the concrete `google_ai.Content` representation -/// required by the `google_cloud_ai_generativelanguage_v1beta` package. -class GoogleContentConverter { - /// Converts a list of `ChatMessage` objects to a list of - /// `google_ai.Content` objects. - List toGoogleAiContent(Iterable messages) { - final result = []; - for (final message in messages) { - final (String? role, List parts) = switch (message) { - UserMessage() => ('user', _convertParts(message.parts)), - UserUiInteractionMessage() => ('user', _convertParts(message.parts)), - AiTextMessage() => ('model', _convertParts(message.parts)), - ToolResponseMessage() => ('user', _convertToolResults(message.results)), - AiUiMessage() => ('model', _convertParts(message.parts)), - InternalMessage() => (null, []), // Not sent to model - }; - - if (role != null && parts.isNotEmpty) { - result.add(google_ai.Content(role: role, parts: parts)); - } - } - return result; - } - - List _convertParts(List parts) { - final result = []; - for (final part in parts) { - switch (part) { - case TextPart(): - result.add(google_ai.Part(text: part.text)); - case ImagePart(): - if (part.bytes != null) { - result.add( - google_ai.Part( - inlineData: google_ai.Blob( - mimeType: part.mimeType, - data: part - .bytes!, // Assuming bytes is not null here as per logic - ), - ), - ); - } else if (part.base64 != null) { - result.add( - google_ai.Part( - inlineData: google_ai.Blob( - mimeType: part.mimeType, - data: Uint8List.fromList(base64.decode(part.base64!)), - ), - ), - ); - } else if (part.url != null) { - // Google Cloud API supports file URIs - result.add( - google_ai.Part( - fileData: google_ai.FileData(fileUri: part.url.toString()), - ), - ); - } else { - throw GoogleAiClientException('ImagePart has no data.'); - } - case ToolCallPart(): - result.add( - google_ai.Part( - functionCall: google_ai.FunctionCall( - id: part.id, - name: part.toolName, - args: protobuf.Struct.fromJson(part.arguments), - ), - ), - ); - case ToolResultPart(): - result.add( - google_ai.Part( - functionResponse: google_ai.FunctionResponse( - id: part.callId, - // ToolResultPart will be removed in the future. - // Function calling history is managed within the - // Content Generator. - name: '', - // The result from ToolResultPart is a JSON string - response: protobuf.Struct.fromJson( - jsonDecode(part.result) as Map, - ), - ), - ), - ); - case ThinkingPart(): - // Represent thoughts as text. - result.add(google_ai.Part(text: 'Thinking: ${part.text}')); - case DataPart(): - throw GoogleAiClientException( - 'DataPart is not supported for Google AI conversion.', - ); - } - } - return result; - } - - List _convertToolResults(List parts) { - final result = []; - for (final part in parts) { - if (part is ToolResultPart) { - result.add( - google_ai.Part( - functionResponse: google_ai.FunctionResponse( - id: part.callId, - // ToolResultPart will be removed in the future. - // Function calling history is managed within the - // Content Generator. - name: '', - response: protobuf.Struct.fromJson( - jsonDecode(part.result) as Map, - ), - ), - ), - ); - } - } - return result; - } -} diff --git a/packages/genui_google_generative_ai/pubspec.yaml b/packages/genui_google_generative_ai/pubspec.yaml deleted file mode 100644 index 9d2886481..000000000 --- a/packages/genui_google_generative_ai/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: genui_google_generative_ai -description: Integration package for genui and Google Cloud Generative Language API. -version: 0.7.0 -homepage: https://github.com/flutter/genui/tree/main/packages/genui_google_generative_ai -license: BSD-3-Clause -issue_tracker: https://github.com/flutter/genui/issues - -environment: - sdk: ">=3.9.2 <4.0.0" - flutter: ">=3.35.7 <4.0.0" - -resolution: workspace - -dependencies: - flutter: - sdk: flutter - genui: ^0.7.0 - google_cloud_ai_generativelanguage_v1beta: ^0.4.0 - google_cloud_protobuf: ^0.4.0 - json_schema_builder: ^0.1.3 - -dev_dependencies: - dart_flutter_team_lints: ^3.5.2 - flutter_test: - sdk: flutter - logging: ^1.3.0 - - diff --git a/packages/genui_google_generative_ai/test/google_content_converter_test.dart b/packages/genui_google_generative_ai/test/google_content_converter_test.dart deleted file mode 100644 index 537e2806b..000000000 --- a/packages/genui_google_generative_ai/test/google_content_converter_test.dart +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart'; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; - -void main() { - group('GoogleContentConverter', () { - late GoogleContentConverter converter; - - setUp(() { - converter = GoogleContentConverter(); - }); - - test('toGoogleAiContent converts UserMessage with TextPart', () { - final messages = [UserMessage.text('Hello')]; - final result = converter.toGoogleAiContent(messages); - - expect(result, hasLength(1)); - expect(result.first.role, 'user'); - expect(result.first.parts, hasLength(1)); - expect(result.first.parts.first.text, 'Hello'); - }); - - test('toGoogleAiContent converts AiTextMessage with TextPart', () { - final messages = [AiTextMessage.text('Hi there')]; - final result = converter.toGoogleAiContent(messages); - - expect(result, hasLength(1)); - expect(result.first.role, 'model'); - expect(result.first.parts, hasLength(1)); - expect(result.first.parts.first.text, 'Hi there'); - }); - - test('toGoogleAiContent converts AiUiMessage', () { - final definition = UiDefinition(surfaceId: 'testSurface'); - final messages = [AiUiMessage(definition: definition)]; - final result = converter.toGoogleAiContent(messages); - expect(result, hasLength(1)); - expect(result.first.role, 'model'); - }); - - test('toGoogleAiContent skips InternalMessage', () { - final messages = [ - UserMessage.text('Hello'), - const InternalMessage('Internal note'), - AiTextMessage.text('Response'), - ]; - final result = converter.toGoogleAiContent(messages); - - // Should only have 2 messages (user and ai), internal is skipped - expect(result, hasLength(2)); - expect(result[0].role, 'user'); - expect(result[1].role, 'model'); - }); - - test('toGoogleAiContent converts ImagePart with bytes', () { - final imageBytes = Uint8List.fromList([1, 2, 3, 4]); - final messages = [ - UserMessage([ImagePart.fromBytes(imageBytes, mimeType: 'image/png')]), - ]; - final result = converter.toGoogleAiContent(messages); - - expect(result, hasLength(1)); - expect(result.first.parts, hasLength(1)); - final part = result.first.parts.first; - expect(part.inlineData, isNotNull); - expect(part.inlineData!.mimeType, 'image/png'); - }); - - test('toGoogleAiContent converts ImagePart with URL', () { - final messages = [ - UserMessage([ - ImagePart.fromUrl( - Uri.parse('gs://bucket/image.png'), - mimeType: 'image/png', - ), - ]), - ]; - final result = converter.toGoogleAiContent(messages); - - expect(result, hasLength(1)); - expect(result.first.parts, hasLength(1)); - final part = result.first.parts.first; - expect(part.fileData, isNotNull); - expect(part.fileData!.fileUri, 'gs://bucket/image.png'); - }); - - test('toGoogleAiContent converts ToolCallPart', () { - final messages = [ - AiTextMessage([ - const ToolCallPart( - id: 'call-1', - toolName: 'calculator', - arguments: {'operation': 'add', 'a': 1, 'b': 2}, - ), - ]), - ]; - final result = converter.toGoogleAiContent(messages); - - expect(result, hasLength(1)); - expect(result.first.parts, hasLength(1)); - final part = result.first.parts.first; - expect(part.functionCall, isNotNull); - expect(part.functionCall!.id, 'call-1'); - expect(part.functionCall!.name, 'calculator'); - }); - }); -} diff --git a/packages/genui_google_generative_ai/test/google_generative_ai_content_generator_test.dart b/packages/genui_google_generative_ai/test/google_generative_ai_content_generator_test.dart deleted file mode 100644 index 671de9bb7..000000000 --- a/packages/genui_google_generative_ai/test/google_generative_ai_content_generator_test.dart +++ /dev/null @@ -1,295 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui/genui.dart' as genui; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; -import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' - as google_ai; -import 'package:google_cloud_protobuf/protobuf.dart' as protobuf; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -void main() { - group('GoogleGenerativeAiContentGenerator', () { - test('constructor creates instance with required parameters', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - apiKey: 'test-api-key', - ); - - expect(generator, isNotNull); - expect(generator.catalog, catalog); - expect(generator.modelName, 'models/gemini-2.5-flash'); - expect(generator.outputToolName, 'provideFinalOutput'); - }); - - test('constructor accepts custom model name', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - modelName: 'models/gemini-2.5-pro', - apiKey: 'test-api-key', - ); - - expect(generator.modelName, 'models/gemini-2.5-pro'); - }); - - test('constructor accepts custom output tool name', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - outputToolName: 'customOutput', - apiKey: 'test-api-key', - ); - - expect(generator.outputToolName, 'customOutput'); - }); - - test('constructor accepts system instruction', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - systemInstruction: 'You are a helpful assistant', - apiKey: 'test-api-key', - ); - - expect(generator.systemInstruction, 'You are a helpful assistant'); - }); - - test('constructor accepts additional tools', () { - final catalog = const genui.Catalog([]); - final tool = genui.DynamicAiTool>( - name: 'testTool', - description: 'A test tool', - invokeFunction: (args) async => {}, - ); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - additionalTools: [tool], - apiKey: 'test-api-key', - ); - - expect(generator.additionalTools, hasLength(1)); - expect(generator.additionalTools.first.name, 'testTool'); - }); - - test('streams are accessible', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - apiKey: 'test-api-key', - ); - - expect(generator.a2uiMessageStream, isNotNull); - expect(generator.textResponseStream, isNotNull); - expect(generator.errorStream, isNotNull); - expect(generator.isProcessing, isNotNull); - }); - - test('isProcessing starts as false', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - apiKey: 'test-api-key', - ); - - expect(generator.isProcessing.value, isFalse); - }); - - test('dispose closes all streams', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - apiKey: 'test-api-key', - ); - - // Should not throw - expect(generator.dispose, returnsNormally); - }); - - test('token usage starts at zero', () { - final catalog = const genui.Catalog([]); - - final generator = GoogleGenerativeAiContentGenerator( - catalog: catalog, - apiKey: 'test-api-key', - ); - - expect(generator.inputTokenUsage, 0); - expect(generator.outputTokenUsage, 0); - }); - - test('isProcessing is true during request', () async { - final generator = GoogleGenerativeAiContentGenerator( - catalog: const genui.Catalog({}), - serviceFactory: ({required configuration}) { - return FakeGoogleGenerativeService([ - google_ai.GenerateContentResponse( - candidates: [ - google_ai.Candidate( - content: google_ai.Content( - role: 'model', - parts: [ - google_ai.Part( - functionCall: google_ai.FunctionCall( - id: '1', - name: 'provideFinalOutput', - args: protobuf.Struct.fromJson({ - 'output': {'response': 'Hello'}, - }), - ), - ), - ], - ), - finishReason: google_ai.Candidate_FinishReason.stop, - ), - ], - ), - ]); - }, - ); - - expect(generator.isProcessing.value, isFalse); - final future = generator.sendRequest( - genui.UserMessage([const genui.TextPart('Hi')]), - ); - expect(generator.isProcessing.value, isTrue); - await future; - expect(generator.isProcessing.value, isFalse); - }); - - test('can call a tool and return a result', () async { - final generator = GoogleGenerativeAiContentGenerator( - catalog: const genui.Catalog({}), - additionalTools: [ - genui.DynamicAiTool>( - name: 'testTool', - description: 'A test tool', - parameters: dsb.Schema.object(), - invokeFunction: (args) async => {'result': 'tool result'}, - ), - ], - serviceFactory: ({required configuration}) { - return FakeGoogleGenerativeService([ - google_ai.GenerateContentResponse( - candidates: [ - google_ai.Candidate( - content: google_ai.Content( - role: 'model', - parts: [ - google_ai.Part( - functionCall: google_ai.FunctionCall( - id: '1', - name: 'testTool', - args: protobuf.Struct.fromJson({}), - ), - ), - ], - ), - finishReason: google_ai.Candidate_FinishReason.stop, - ), - ], - ), - google_ai.GenerateContentResponse( - candidates: [ - google_ai.Candidate( - content: google_ai.Content( - role: 'model', - parts: [ - google_ai.Part( - functionCall: google_ai.FunctionCall( - id: '2', - name: 'provideFinalOutput', - args: protobuf.Struct.fromJson({ - 'output': {'response': 'Tool called'}, - }), - ), - ), - ], - ), - finishReason: google_ai.Candidate_FinishReason.stop, - ), - ], - ), - ]); - }, - ); - - final hi = genui.UserMessage([const genui.TextPart('Hi')]); - final completer = Completer(); - unawaited(generator.textResponseStream.first.then(completer.complete)); - await generator.sendRequest(hi); - final response = await completer.future; - expect(response, 'Tool called'); - }); - - test('returns a simple text response', () async { - final generator = GoogleGenerativeAiContentGenerator( - catalog: const genui.Catalog({}), - serviceFactory: ({required configuration}) { - return FakeGoogleGenerativeService([ - google_ai.GenerateContentResponse( - candidates: [ - google_ai.Candidate( - content: google_ai.Content( - role: 'model', - parts: [ - google_ai.Part( - functionCall: google_ai.FunctionCall( - id: '1', - name: 'provideFinalOutput', - args: protobuf.Struct.fromJson({ - 'output': {'response': 'Hello'}, - }), - ), - ), - ], - ), - finishReason: google_ai.Candidate_FinishReason.stop, - ), - ], - ), - ]); - }, - ); - - final hi = genui.UserMessage([const genui.TextPart('Hi')]); - final completer = Completer(); - unawaited(generator.textResponseStream.first.then(completer.complete)); - await generator.sendRequest(hi); - final response = await completer.future; - expect(response, 'Hello'); - }); - }); -} - -class FakeGoogleGenerativeService implements GoogleGenerativeServiceInterface { - FakeGoogleGenerativeService(this.responses); - - final List responses; - int callCount = 0; - - @override - Future generateContent( - google_ai.GenerateContentRequest request, - ) { - return Future.delayed(Duration.zero, () => responses[callCount++]); - } - - @override - void close() { - // No-op for testing - } -} diff --git a/packages/genui_google_generative_ai/test/google_schema_adapter_test.dart b/packages/genui_google_generative_ai/test/google_schema_adapter_test.dart deleted file mode 100644 index c8731613c..000000000 --- a/packages/genui_google_generative_ai/test/google_schema_adapter_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; -import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart' - as google_ai; -import 'package:json_schema_builder/json_schema_builder.dart' as dsb; - -void main() { - group('GoogleSchemaAdapter', () { - late GoogleSchemaAdapter adapter; - - setUp(() { - adapter = GoogleSchemaAdapter(); - }); - - test('adapt converts string schema', () { - final schema = dsb.S.string(); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.string); - expect(result.errors, isEmpty); - }); - - test('adapt converts number schema', () { - final schema = dsb.S.number(); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.number); - expect(result.errors, isEmpty); - }); - - test('adapt converts integer schema', () { - final schema = dsb.S.integer(); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.integer); - expect(result.errors, isEmpty); - }); - - test('adapt converts boolean schema', () { - final schema = dsb.S.boolean(); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.boolean); - expect(result.errors, isEmpty); - }); - - test('adapt converts object schema with properties', () { - final schema = dsb.S.object( - properties: {'name': dsb.S.string(), 'age': dsb.S.integer()}, - required: ['name'], - ); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.object); - expect(result.schema!.properties, hasLength(2)); - expect(result.schema!.properties['name']!.type, google_ai.Type.string); - expect(result.schema!.properties['age']!.type, google_ai.Type.integer); - expect(result.schema!.required, contains('name')); - expect(result.errors, isEmpty); - }); - - test('adapt converts array schema', () { - final schema = dsb.S.list(items: dsb.S.string()); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.array); - expect(result.schema!.items, isNotNull); - expect(result.schema!.items!.type, google_ai.Type.string); - expect(result.errors, isEmpty); - }); - - test('adapt converts nested object schema', () { - final schema = dsb.S.object( - properties: { - 'user': dsb.S.object( - properties: {'name': dsb.S.string(), 'email': dsb.S.string()}, - ), - }, - ); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.object); - expect(result.schema!.properties, hasLength(1)); - final userSchema = result.schema!.properties['user']!; - expect(userSchema.type, google_ai.Type.object); - expect(userSchema.properties, hasLength(2)); - expect(result.errors, isEmpty); - }); - - test('adapt adds error for unsupported keyword', () { - final schema = dsb.Schema.fromMap({ - 'type': 'string', - '\$ref': '#/definitions/something', - }); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.errors, isNotEmpty); - expect(result.errors.any((e) => e.message.contains('\$ref')), isTrue); - }); - - test('adapt handles string with enum values', () { - final schema = dsb.S.string(enumValues: ['red', 'green', 'blue']); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.type, google_ai.Type.string); - expect(result.schema!.enum$, isNotNull); - expect(result.schema!.enum$, hasLength(3)); - expect(result.schema!.enum$, containsAll(['red', 'green', 'blue'])); - }); - - test('adapt handles schema with description', () { - final schema = dsb.S.string(description: 'A test string'); - final result = adapter.adapt(schema); - - expect(result.schema, isNotNull); - expect(result.schema!.description, 'A test string'); - }); - - test('adapt handles schema without type returns error', () { - final schema = dsb.Schema.fromMap({'description': 'No type'}); - final result = adapter.adapt(schema); - - expect(result.schema, isNull); - expect(result.errors, isNotEmpty); - }); - - test('adapt adds error for array without items', () { - final schema = dsb.Schema.fromMap({'type': 'array'}); - final result = adapter.adapt(schema); - - expect(result.schema, isNull); - expect(result.errors, isNotEmpty); - expect(result.errors.any((e) => e.message.contains('items')), isTrue); - }); - }); -} diff --git a/packages/json_schema_builder/bin/schema_validator.dart b/packages/json_schema_builder/bin/schema_validator.dart index 80fc01062..9219d6139 100644 --- a/packages/json_schema_builder/bin/schema_validator.dart +++ b/packages/json_schema_builder/bin/schema_validator.dart @@ -7,14 +7,21 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; Future main(List arguments) async { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + stdout.writeln(record.message); + }); + final log = Logger('SchemaValidator'); + final parser = ArgParser()..addOption('schema', abbr: 's', mandatory: true); final ArgResults argResults = parser.parse(arguments); final schemaFile = File(argResults['schema'] as String); if (!schemaFile.existsSync()) { - print('Error: Schema file not found: ${schemaFile.path}'); + log.severe('Error: Schema file not found: ${schemaFile.path}'); exit(1); } @@ -23,29 +30,32 @@ Future main(List arguments) async { final schema = Schema.fromMap(schemaJson); if (argResults.rest.isEmpty) { - print('No JSON files provided to validate.'); + if (argResults.rest.isEmpty) { + log.info('No JSON files provided to validate.'); + return; + } return; } for (final String filePath in argResults.rest) { final file = File(filePath); if (!file.existsSync()) { - print('Error: JSON file not found: ${file.path}'); + log.severe('Error: JSON file not found: ${file.path}'); continue; } - print('Validating ${file.path}...'); + log.info('Validating ${file.path}...'); final String fileContent = file.readAsStringSync(); final Object? jsonData = jsonDecode(fileContent); final List errors = await schema.validate(jsonData); if (errors.isEmpty) { - print(' SUCCESS: ${file.path} is valid.'); + log.info(' SUCCESS: ${file.path} is valid.'); } else { - print(' FAILURE: ${file.path} is invalid:'); + log.severe(' FAILURE: ${file.path} is invalid:'); for (final error in errors) { - print(' - ${error.toErrorString()}'); + log.severe(' - ${error.toErrorString()}'); } } } diff --git a/packages/json_schema_builder/example/build_and_validate_schema.dart b/packages/json_schema_builder/example/build_and_validate_schema.dart index ad88b233d..96f8e9fdb 100644 --- a/packages/json_schema_builder/example/build_and_validate_schema.dart +++ b/packages/json_schema_builder/example/build_and_validate_schema.dart @@ -2,9 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; + import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:logging/logging.dart'; Future main() async { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + stdout.writeln(record.message); + }); + final log = Logger('BuildAndValidateSchema'); // This example demonstrates how to build a complex, interesting schema to // validate a "System Event". Our system can have different types of events, // and we want to ensure that the data for each event is structured correctly. @@ -106,7 +114,7 @@ Future main() async { // 3. Create Sample Data and Validate It // ========================================================================= - print('--- 1. Validating a Correct Login Event ---'); + log.info('--- 1. Validating a Correct Login Event ---'); final Map validLoginEvent = { 'eventId': 'a1b2c3d4-e5f6-7890-1234-567890abcdef', 'timestamp': '2025-07-28T10:00:00Z', @@ -115,7 +123,7 @@ Future main() async { }; await validateAndPrintResults(systemEventSchema, validLoginEvent); - print('\n--- 2. Validating a Correct File Upload Event ---'); + log.info('\n--- 2. Validating a Correct File Upload Event ---'); final Map validFileUploadEvent = { 'eventId': 'b2c3d4e5-f6a7-8901-2345-67890abcdef1', 'timestamp': '2025-07-28T11:30:00Z', @@ -134,7 +142,7 @@ Future main() async { }; await validateAndPrintResults(systemEventSchema, validFileUploadEvent); - print('\n--- 3. Validating an Invalid Event (Multiple Errors) ---'); + log.info('\n--- 3. Validating an Invalid Event (Multiple Errors) ---'); final Map invalidEvent = { 'eventId': 'not-a-uuid', // Fails pattern 'timestamp': '2025-07-28 12:00:00', // Fails pattern (not ISO 8601) @@ -147,7 +155,7 @@ Future main() async { }; await validateAndPrintResults(systemEventSchema, invalidEvent); - print('\n--- 4. Validating an Invalid Login Event Payload ---'); + log.info('\n--- 4. Validating an Invalid Login Event Payload ---'); final Map invalidLoginPayload = { 'eventId': 'c3d4e5f6-a7b8-9012-3456-7890abcdef12', 'timestamp': '2025-07-28T14:00:00Z', @@ -167,16 +175,17 @@ Future validateAndPrintResults( Schema schema, Map data, ) async { + final log = Logger('BuildAndValidateSchema'); final List errors = await schema.validate(data); if (errors.isEmpty) { - print('✅ Success! The data is valid.'); + log.info('✅ Success! The data is valid.'); } else { - print('❌ Failure! The data is invalid. Found ${errors.length} errors:'); + log.info('❌ Failure! The data is invalid. Found ${errors.length} errors:'); for (final error in errors) { // The toErrorString() method provides a human-readable summary of the // error. - print(' - ${error.toErrorString()}'); + log.info(' - ${error.toErrorString()}'); } } } diff --git a/packages/json_schema_builder/lib/src/schema_validation.dart b/packages/json_schema_builder/lib/src/schema_validation.dart index 4e29e2658..a224e0a91 100644 --- a/packages/json_schema_builder/lib/src/schema_validation.dart +++ b/packages/json_schema_builder/lib/src/schema_validation.dart @@ -973,9 +973,7 @@ extension SchemaValidation on Schema { .vocabularies['https://json-schema.org/draft/2020-12/vocab/validation'] == true) { final int matchCount = matches.length; - if (listSchema.minContains == 0 && data.isEmpty) { - // This is a valid case. - } else if (matchCount == 0 && + if (matchCount == 0 && (listSchema.minContains == null || listSchema.minContains! > 0)) { errors.add( ValidationError( diff --git a/packages/json_schema_builder/pubspec.yaml b/packages/json_schema_builder/pubspec.yaml index c8d7e715d..b72f3e943 100644 --- a/packages/json_schema_builder/pubspec.yaml +++ b/packages/json_schema_builder/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: decimal: ^3.2.4 email_validator: ^3.0.0 http: ^1.6.0 + logging: ^1.3.0 dev_dependencies: mockito: ^5.5.0 diff --git a/pubspec.yaml b/pubspec.yaml index 029ee15de..7b6dc81c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,17 +12,13 @@ environment: workspace: - examples/catalog_gallery - - examples/custom_backend - examples/simple_chat + - examples/travel_app - examples/verdure/client - packages/genui - packages/genui_a2ui - - packages/genui_a2ui/example - - packages/genui_dartantic - - packages/genui_dartantic/example - - packages/genui_firebase_ai - - packages/genui_google_generative_ai + - packages/json_schema_builder - tool/fix_copyright - tool/release diff --git a/tool/test_and_fix/bin/test_and_fix.dart b/tool/test_and_fix/bin/test_and_fix.dart index b17308377..a3ec5eed4 100644 --- a/tool/test_and_fix/bin/test_and_fix.dart +++ b/tool/test_and_fix/bin/test_and_fix.dart @@ -2,10 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; + import 'package:args/args.dart'; +import 'package:logging/logging.dart'; import 'package:test_and_fix/test_and_fix.dart'; Future main(List arguments) async { + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + stdout.writeln(record.message); + }); + final parser = ArgParser() ..addFlag( 'help', @@ -32,7 +40,7 @@ Future main(List arguments) async { final ArgResults argResults = parser.parse(arguments); if (argResults['help'] as bool) { - print(parser.usage); + stdout.writeln(parser.usage); return 0; } diff --git a/tool/test_and_fix/lib/test_and_fix.dart b/tool/test_and_fix/lib/test_and_fix.dart index 7c5e8dbb1..87d5485aa 100644 --- a/tool/test_and_fix/lib/test_and_fix.dart +++ b/tool/test_and_fix/lib/test_and_fix.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:file/file.dart'; import 'package:file/local.dart'; +import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; import 'package:process/process.dart'; import 'package:process_runner/process_runner.dart'; @@ -14,12 +15,15 @@ class TestAndFix { TestAndFix({ this.fs = const LocalFileSystem(), ProcessManager? processManager, + Logger? logger, }) : processRunner = ProcessRunner( processManager: processManager ?? const LocalProcessManager(), - ); + ), + _log = logger ?? Logger('TestAndFix'); final FileSystem fs; final ProcessRunner processRunner; + final Logger _log; Future run({ Directory? root, @@ -77,7 +81,9 @@ class TestAndFix { } } - print('Found ${projects.length} projects and created ${jobs.length} jobs.'); + _log.info( + 'Found ${projects.length} projects and created ${jobs.length} jobs.', + ); final pool = ProcessPool( numWorkers: Platform.numberOfProcessors, @@ -93,26 +99,26 @@ class TestAndFix { .where((job) => job.result.exitCode != 0) .toList(); - print('--- Successful Jobs ---'); + _log.info('\n--- Successful Jobs ---'); for (final job in successfulJobs) { - print(' - ${job.name} (exit code ${job.result.exitCode})'); + _log.info(' - ${job.name} (exit code ${job.result.exitCode})'); if (verbose && job.result.output.isNotEmpty) { - print(job.result.output); + _log.info(job.result.output); } } if (failedJobs.isNotEmpty) { - print('\n--- Failed Jobs ---'); + _log.severe('\n--- Failed Jobs ---'); for (final job in failedJobs) { - print(' - ${job.name} (exit code ${job.result.exitCode})'); + _log.severe(' - ${job.name} (exit code ${job.result.exitCode})'); if (job.result.output.isNotEmpty) { - print(job.result.output); + _log.severe(job.result.output); } } return false; } - print('\nAll jobs completed successfully!'); + _log.info('\nAll jobs completed successfully!'); return true; } @@ -147,7 +153,7 @@ class TestAndFix { } } } on FileSystemException catch (exception) { - print( + _log.warning( 'Warning: Failed to list directory contents while searching for ' 'projects: $exception', ); diff --git a/tool/test_and_fix/pubspec.yaml b/tool/test_and_fix/pubspec.yaml index 02de100e8..3d6dd14b3 100644 --- a/tool/test_and_fix/pubspec.yaml +++ b/tool/test_and_fix/pubspec.yaml @@ -16,6 +16,7 @@ resolution: workspace dependencies: args: ^2.7.0 file: ^7.0.1 + logging: ^1.3.0 path: ^1.9.1 process: ^5.0.5 process_runner: ^4.2.4 diff --git a/tool/test_and_fix/test/test_and_fix_test.dart b/tool/test_and_fix/test/test_and_fix_test.dart index b0bfdb8da..145198a67 100644 --- a/tool/test_and_fix/test/test_and_fix_test.dart +++ b/tool/test_and_fix/test/test_and_fix_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:file/memory.dart'; import 'package:file/src/interface/directory.dart'; +import 'package:logging/logging.dart'; import 'package:process_runner/test/fake_process_manager.dart'; import 'package:test/test.dart'; import 'package:test_and_fix/test_and_fix.dart'; @@ -20,7 +21,7 @@ void main() { setUp(() { fs = MemoryFileSystem(); processManager = FakeProcessManager((input) { - print('Stdin supplied: $input'); + stdout.writeln('Stdin supplied: $input'); }); testAndFix = TestAndFix(fs: fs, processManager: processManager); }); @@ -234,21 +235,26 @@ void main() { ProcessResult(0, 0, '', ''), ], }; - final printOutput = []; - await runZoned( - () async { - await testAndFix.run(root: root); - }, - zoneSpecification: ZoneSpecification( - print: (Zone self, ZoneDelegate parent, Zone zone, String message) { - printOutput.add(message); - }, - ), + final logRecords = []; + hierarchicalLoggingEnabled = true; + final logger = Logger('TestAndFix'); + logger.level = Level.ALL; + final StreamSubscription sub = logger.onRecord.listen( + (r) => logRecords.add(r.message), + ); + addTearDown(sub.cancel); + + testAndFix = TestAndFix( + fs: fs, + processManager: processManager, + logger: logger, ); - expect(printOutput, contains('\n--- Failed Jobs ---')); + await testAndFix.run(root: root); + + expect(logRecords, contains('\n--- Failed Jobs ---')); expect( - printOutput.any((line) => line.contains('dart fix (exit code 1)')), + logRecords.any((line) => line.contains('dart fix (exit code 1)')), isTrue, ); });