Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
analyze-and-test:
name: Analyze & Test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Flutter version
run: flutter --version

- name: Install dependencies
run: flutter pub get

- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze
run: flutter analyze --fatal-infos

- name: Run tests
run: flutter test --coverage

- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/lcov.info
if-no-files-found: ignore
57 changes: 29 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Every source file must include the BSD-3-Clause license header:
### Imports

- Use package imports for external packages: `import 'package:genui/genui.dart';`
- Use relative imports for internal files: `import 'claude_config.dart';`
- Use relative imports for internal files: `import 'genui_x_config.dart';`
- Group imports in this order: dart:, external packages, internal packages
- Use `as` prefix for package aliases when needed

Expand All @@ -83,16 +83,16 @@ import 'dart:convert';
import 'package:genui/genui.dart';
import 'package:http/http.dart' as http;

import 'claude_config.dart';
import 'sse_parser.dart';
import 'anthropic_sse_parser.dart';
import 'genui_x_config.dart';
```

### Naming Conventions

- **Classes**: `PascalCase` (e.g., `ClaudeTransport`, `ClaudeApiException`)
- **Classes**: `PascalCase` (e.g., `GenuiXTransport`, `GenuiXApiError`)
- **Constants**: `camelCase` with `k` prefix for const objects (e.g., `kDefaultModel`)
- **Variables/Methods**: `camelCase` (e.g., `apiKey`, `sendRequest`)
- **Files**: `snake_case` (e.g., `claude_transport.dart`, `sse_parser.dart`)
- **Files**: `snake_case`. Public/package-namespaced files use the `genui_x_` prefix (e.g., `genui_x_transport.dart`, `genui_x_config.dart`); per-vendor parsers use the vendor name (e.g., `anthropic_sse_parser.dart`, `openai_sse_parser.dart`, `gemini_sse_parser.dart`)
- **Private members**: prefix with `_` (e.g., `_config`, `_history`)

### Type Annotations
Expand All @@ -103,8 +103,8 @@ import 'sse_parser.dart';

```dart
// Good
class ClaudeConfig {
const ClaudeConfig({
class GenuiXConfig {
const GenuiXConfig({
required this.apiKey,
this.model = 'claude-haiku-4-5-20251001',
});
Expand All @@ -121,15 +121,14 @@ class ClaudeConfig {
- Keep documentation concise but complete

```dart
/// A [Transport] implementation that uses Anthropic's Claude API.
///
/// Connects the genui framework to Claude by generating A2UI JSON messages.
class ClaudeTransport implements Transport {
/// Creates a [ClaudeTransport].
/// A [Transport] implementation that streams A2UI JSON from a configurable
/// LLM backend (Anthropic, OpenAI-compatible, or Gemini).
class GenuiXTransport implements Transport {
/// Creates a [GenuiXTransport].
///
/// [apiKey] is required. All other parameters are optional.
/// [catalog] defines the UI components the AI can generate.
ClaudeTransport({
GenuiXTransport({
required String apiKey,
required Catalog catalog,
});
Expand All @@ -143,14 +142,14 @@ class ClaudeTransport implements Transport {
- Prefer specific exception types over generic `Exception`

```dart
class ClaudeApiException implements Exception {
const ClaudeApiException(this.statusCode, this.message);
class GenuiXApiError implements Exception {
const GenuiXApiError(this.statusCode, this.message);

final int statusCode;
final String message;

@override
String toString() => 'ClaudeApiException($statusCode): $message';
String toString() => 'GenuiXApiError($statusCode): $message';
}
```

Expand All @@ -167,9 +166,9 @@ class ClaudeApiException implements Exception {
- Use descriptive test names that explain expected behavior

```dart
group('ClaudeConfig', () {
group('GenuiXConfig', () {
test('has correct defaults', () {
const config = ClaudeConfig(apiKey: 'test-key');
const config = GenuiXConfig(apiKey: 'test-key');
expect(config.model, 'claude-haiku-4-5-20251001');
});
});
Expand All @@ -183,12 +182,12 @@ group('ClaudeConfig', () {

```dart
try {
await for (final chunk in _streamClaude()) {
await for (final chunk in _streamLlm()) {
_adapter.addChunk(chunk);
}
} on ClaudeAuthException {
} on GenuiXAuthError {
rethrow;
} on ClaudeApiException catch (e) {
} on GenuiXApiError catch (e) {
_adapter.addChunk('\n\nSorry, I encountered an error: ${e.message}');
}
```
Expand All @@ -199,7 +198,7 @@ try {

The genui framework (`package:genui` ^0.8.0) uses the **A2UI v0.9 protocol** for AI→UI communication:

1. **`createSurface` before `updateComponents`**: Claude must always emit `createSurface` (with `surfaceId` + `catalogId`) before `updateComponents`. `SurfaceController` buffers `updateComponents` for unknown surfaces — if `createSurface` never arrives, nothing renders. `PromptBuilder.chat` already instructs this; never override it in user messages.
1. **`createSurface` before `updateComponents`**: The model must always emit `createSurface` (with `surfaceId` + `catalogId`) before `updateComponents`. `SurfaceController` buffers `updateComponents` for unknown surfaces — if `createSurface` never arrives, nothing renders. `PromptBuilder.chat` already instructs this; never override it in user messages.

2. **`"id"` is required in every component**: `Component.fromJson` hard-casts `json['id'] as String`. A missing `id` throws `type 'Null' is not a subtype of type 'String'`. Always include `"id"` in JSON examples in system prompt fragments.

Expand Down Expand Up @@ -233,16 +232,18 @@ Example well-formed `updateComponents`:
| File | Purpose |
|------|---------|
| `lib/genui_x.dart` | Public exports |
| `lib/src/claude_transport.dart` | Main `Transport` implementation |
| `lib/src/claude_config.dart` | Configuration class |
| `lib/src/sse_parser.dart` | SSE stream parser |
| `lib/src/genui_x_transport.dart` | Main `Transport` implementation (`GenuiXTransport`) with `.openai()`, `.anthropic()`, `.gemini()` factories |
| `lib/src/genui_x_config.dart` | `GenuiXConfig` value object and `GenuiXStreamFormat` enum |
| `lib/src/anthropic_sse_parser.dart` | Anthropic Messages API SSE parser |
| `lib/src/openai_sse_parser.dart` | OpenAI Chat Completions SSE parser |
| `lib/src/gemini_sse_parser.dart` | Gemini `streamGenerateContent` SSE parser |

### Key Interfaces

- `Transport` - genui's transport interface (from `package:genui`)
- `ClaudeTransport` - main implementation
- `ClaudeConfig` - configuration holder
- `ClaudeSseParser` - parses Server-Sent Events from Claude API
- `GenuiXTransport` - main implementation
- `GenuiXConfig` - configuration holder
- `AnthropicSseParser` / `OpenAiSseParser` / `GeminiSseParser` - per-vendor SSE parsers (internal)

### Dependencies

Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
## 0.0.11

* Add `GenuiXTransport.gemini()` factory — pre-configures Google's Generative
Language API endpoint (`/v1beta/models/{model}:streamGenerateContent?alt=sse`),
the `x-goog-api-key` header, and the Gemini SSE format. Works with
`gemini-2.5-flash`, `gemini-2.5-pro`, and Vertex AI / proxy endpoints that
mirror the Generative Language API surface.
* Add `GenuiXStreamFormat.gemini` and a Gemini SSE parser (`GeminiSseParser`)
that extracts text from `candidates[*].content.parts[*].text`.
* Add `enforceJsonMode` option on `GenuiXTransport.openai()` and
`GenuiXConfig` — when `true`, injects `response_format: {"type": "json_object"}`
for tighter A2UI compliance on OpenAI and OpenAI-compatible backends.
Respects user-supplied `requestBodyOverrides['response_format']`.
* Add `example/lib/gemini_main.dart` — runnable Gemini chat demo.
* Add GitHub Actions CI (`.github/workflows/ci.yml`) running
`flutter analyze` and `flutter test` on push and pull request.
* Refactor request building: extract `_buildUri()` and `_buildPayload()`
in `GenuiXTransport` to keep provider-specific logic isolated. The
Anthropic and OpenAI request shapes are unchanged; this is a non-breaking
internal cleanup that supports the new Gemini path.
* Internal rename pass — no public API change. `lib/src/claude_transport.dart`
→ `genui_x_transport.dart`, `lib/src/claude_config.dart` →
`genui_x_config.dart`, `lib/src/sse_parser.dart` →
`anthropic_sse_parser.dart` (class `ClaudeSseParser` → `AnthropicSseParser`).
Internal `_streamClaude()` / `_toClaudeMessage()` / `_sseParser` are now
`_streamLlm()` / `_toMessage()` / `_anthropicSseParser`. Test files renamed
to match. AGENTS.md docs updated. The public surface
(`GenuiXTransport`, `GenuiXConfig`, `GenuiXStreamFormat`, errors) is
unchanged.

## 0.0.10

* Add `GenuiXTransport.anthropic()` factory constructor — mirrors `.openai()` with explicit Anthropic defaults (`x-api-key` header, `/v1/messages` endpoint, Anthropic SSE format).
* Add automatic retry on 429 responses with exponential backoff — configurable via `maxRetries` (default `3`). Respects `Retry-After` header when present.

## 0.0.9

* Add `GenuiXTransport.openai()` factory constructor — pre-configures `Authorization: Bearer` header, `/v1/chat/completions` endpoint, and OpenAI SSE format. Works with OpenAI, OpenRouter, LiteLLM, and any OpenAI-compatible proxy.
Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Your App
# pubspec.yaml
dependencies:
genui: ^0.8.0
genui_x: ^0.0.10
genui_x: ^0.0.11
```

### 2. Create your catalog
Expand Down Expand Up @@ -128,10 +128,30 @@ final transport = GenuiXTransport(
final transport = GenuiXTransport.openai(
apiKey: 'sk-your-openai-key',
catalog: myCatalog,
// model: 'gpt-4o', // optional — default is gpt-4o-mini
// model: 'gpt-4o', // optional — default is gpt-4o-mini
// enforceJsonMode: true, // optional — sets response_format to json_object
);
```

Set `enforceJsonMode: true` to pin OpenAI's response to a JSON object, which
improves A2UI compliance on smaller models. Has no effect on Anthropic or
Gemini transports.

### Google Gemini

```dart
final transport = GenuiXTransport.gemini(
apiKey: 'your-google-api-key',
catalog: myCatalog,
// model: 'gemini-2.5-pro', // optional — default is gemini-2.5-flash
);
```

Sends `x-goog-api-key`, posts to
`/v1beta/models/{model}:streamGenerateContent?alt=sse`, and parses Gemini's
`candidates[*].content.parts[*].text` SSE stream. Override `baseUrl` to
point at a Vertex AI gateway or your own proxy.

### OpenRouter / LiteLLM / custom proxy

```dart
Expand Down Expand Up @@ -242,6 +262,8 @@ final transport = GenuiXTransport(
| Claude | `claude-opus-4-6` | Highest quality |
| OpenAI | `gpt-4o-mini` | Default for `.openai()` |
| OpenAI | `gpt-4o` | Higher quality |
| Gemini | `gemini-2.5-flash` | Default for `.gemini()` — fast, low cost |
| Gemini | `gemini-2.5-pro` | Higher quality |
| OpenRouter | any model slug | via `GenuiXTransport.openai(baseUrl: ...)` |

---
Expand All @@ -263,6 +285,10 @@ flutter run -t lib/proxy_main.dart \
--dart-define=PROXY_BASE_URL=https://openrouter.ai/api \
--dart-define=PROXY_API_KEY=sk-or-your-key \
--dart-define=PROXY_MODEL=anthropic/claude-3.5-sonnet

# Google Gemini
flutter run -t lib/gemini_main.dart \
--dart-define=GEMINI_API_KEY=your-google-api-key
```

---
Expand Down
9 changes: 5 additions & 4 deletions example/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-04-06 | Updated: 2026-04-06 -->
<!-- Generated: 2026-04-06 | Updated: 2026-04-19 -->

# example/

## Purpose
Standalone Flutter app demonstrating three usage patterns of `genui_x`. Each entry point is a self-contained demo that can be launched independently with `flutter run -t lib/<entry>.dart`. The example app uses a separate `pubspec.yaml` and is not part of the published package.
Standalone Flutter app demonstrating four usage patterns of `genui_x` (minimal Anthropic, full-featured Anthropic with proxy/model overrides, custom catalog with travel widgets, and Gemini backend). Each entry point is a self-contained demo that can be launched independently with `flutter run -t lib/<entry>.dart`. The example app uses a separate `pubspec.yaml` and is not part of the published package.

## Subdirectories

Expand All @@ -23,8 +23,9 @@ Standalone Flutter app demonstrating three usage patterns of `genui_x`. Each ent
flutter run -t lib/main.dart --dart-define=CLAUDE_API_KEY=sk-ant-...
flutter run -t lib/minimal_main.dart --dart-define=CLAUDE_API_KEY=sk-ant-...
flutter run -t lib/travel_main.dart --dart-define=CLAUDE_API_KEY=sk-ant-...
flutter run -t lib/gemini_main.dart --dart-define=GEMINI_API_KEY=...
```
- To use a proxy: add `--dart-define=CLAUDE_BASE_URL=https://...` and optionally `CLAUDE_STREAM_FORMAT=openai`, `CLAUDE_ENDPOINT_PATH`, `CLAUDE_API_KEY_HEADER`, `CLAUDE_API_KEY_PREFIX`.
- To use a proxy with the Anthropic-shaped demos: add `--dart-define=CLAUDE_BASE_URL=https://...` and optionally `CLAUDE_STREAM_FORMAT=openai`, `CLAUDE_ENDPOINT_PATH`, `CLAUDE_API_KEY_HEADER`, `CLAUDE_API_KEY_PREFIX`.
- The `ios/` directory is auto-generated — never edit files there directly.

### Testing Requirements
Expand All @@ -33,7 +34,7 @@ cd example && flutter test
```

### Common Patterns
- All three entry points follow the same structure: `ClaudeTransport` → `SurfaceController` → `Conversation` → listen to `events`, render `surfaces`.
- All entry points follow the same structure: `GenuiXTransport` (or one of its `.openai()` / `.anthropic()` / `.gemini()` factories) → `SurfaceController` → `Conversation` → listen to `events`, render `surfaces`.
- `_sendSample()` in each example sends a plain natural-language user message; do NOT embed raw A2UI JSON in user messages — the system prompt generated by `PromptBuilder` handles protocol instruction.

## Dependencies
Expand Down
15 changes: 8 additions & 7 deletions example/lib/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<!-- Parent: ../AGENTS.md -->
<!-- Generated: 2026-04-06 | Updated: 2026-04-06 -->
<!-- Generated: 2026-04-06 | Updated: 2026-04-19 -->

# example/lib/

## Purpose
Three Flutter entry points demonstrating different complexity levels of `genui_x` integration. Each file is a complete runnable app including its own `Catalog`, `CatalogItem`, `widgetBuilder`, and chat UI.
Four Flutter entry points demonstrating different complexity levels and backend choices for `genui_x` integration. Each file is a complete runnable app including its own `Catalog`, `CatalogItem`, `widgetBuilder`, and chat UI.

## Key Files

Expand All @@ -13,15 +13,16 @@ Three Flutter entry points demonstrating different complexity levels of `genui_x
| `minimal_main.dart` | Simplest demo — `WeatherWidget` catalog, single-button chat, no proxy options |
| `main.dart` | Full-featured demo — `WeatherWidget` catalog with proxy/model overrides via `--dart-define`, text input chat |
| `travel_main.dart` | Travel demo — custom `TravelPlanWidget` catalog, shows `data`-wrapped component properties and list props |
| `gemini_main.dart` | Gemini-backend demo — uses `GenuiXTransport.gemini()` against the Generative Language API |

## For AI Agents

### A2UI Protocol — CRITICAL RULES
The `PromptBuilder.chat` used in `ClaudeTransport` generates a system prompt that instructs Claude to:
The `PromptBuilder.chat` used in `GenuiXTransport` generates a system prompt that instructs the model to:
1. First send a `createSurface` message (with unique `surfaceId` and `catalogId`).
2. Then send an `updateComponents` message populating the surface.

**Never override this by telling Claude to skip `createSurface` in user messages.**
**Never override this by telling the model to skip `createSurface` in user messages.**
The `SurfaceController` buffers `updateComponents` for unknown surfaces — if `createSurface` never arrives, nothing renders.

Component objects in `updateComponents` **MUST** include `"id"` (a unique string). At least one component **MUST** have `"id": "root"` — without it nothing displays. The parser hard-casts `json['id'] as String`; a missing `id` causes a `type 'Null' is not a subtype of type 'String'` crash.
Expand All @@ -39,13 +40,13 @@ The `widgetBuilder` accesses them via `ctx.data as Map<String, dynamic>`.

### systemPromptFragments guidelines
- Use fragments to describe **when/why** to use a widget, and to show the correct `data` shape.
- Do NOT instruct Claude to skip `createSurface` or to "respond ONLY with updateComponents" — that fights the framework.
- Do NOT instruct the model to skip `createSurface` or to "respond ONLY with updateComponents" — that fights the framework.
- Include `"id": "root"` in any JSON examples embedded in fragments.

### Working In This Directory
- Each file is self-contained. New demos can be added as new `*_main.dart` files.
- Keep `widgetBuilder` logic minimal — it should only cast and delegate to a proper `StatelessWidget`.
- Use `(data['field'] as num).toInt()` / `.toDouble()` for numeric fields — Claude may emit integers or floats.
- Use `(data['field'] as num).toInt()` / `.toDouble()` for numeric fields — the model may emit integers or floats.

### Testing Requirements
```bash
Expand All @@ -59,7 +60,7 @@ cd example && flutter test # widget smoke tests
- `../AGENTS.md` — example-wide conventions

### External
- `package:genui_x` — `ClaudeTransport`, `ClaudeStreamFormat`
- `package:genui_x` — `GenuiXTransport` (with `.openai()`, `.anthropic()`, `.gemini()` factories), `GenuiXConfig`, `GenuiXStreamFormat`
- `package:genui` — `Catalog`, `CatalogItem`, `SurfaceController`, `Conversation`, `Surface`, `ChatMessage`, `PromptBuilder`
- `package:json_schema_builder` — `S.object()`, `S.string()`, etc.

Expand Down
Loading
Loading