Skip to content

Revive the cli ai commands on pydantic-ai 2.x with validated output and offline tests - #55

Merged
jaykv merged 5 commits into
mainfrom
feat/revive-ai-commands
Oct 12, 2026
Merged

jaykv merged 5 commits into
mainfrom
feat/revive-ai-commands

Conversation

@jaykv

@jaykv jaykv commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Stack 4 of 6 (more to follow). Base: fix/audit-bugs-and-security. Merge in order; each PR shows only its own changes.

Summary

cli ai generate and cli ai ask work again on the current pydantic-ai (2.55 in the lockfile; the ai extra floor stays pydantic-ai>=2.0.0 from the base branch). Model choice is provider-agnostic, generated manifests are validated before they are written, and both commands are tested offline. Stacked on chore/python-3.11-3.15.

What changed

cliffy/ai.py (rewritten)

  • Lazy import. pydantic-ai is only imported when a command actually talks to a model. import cliffy.cli loads no pydantic_ai module.

  • Model selection. Order: --model / -m, then CLIFFY_AI_MODEL, then a default for the first provider whose API key is set:

    API key Default model
    GOOGLE_API_KEY or GEMINI_API_KEY google:gemini-flash-latest
    ANTHROPIC_API_KEY anthropic:claude-sonnet-5-5
    OPENAI_API_KEY openai:gpt-5.4-mini

    With no key set it falls back to google:gemini-flash-latest. All three ids are in the model list of the installed pydantic-ai 2.55.

  • Clear errors. A missing extra exits with pip install 'cliffy[ai]'. A missing API key, unknown model or missing provider SDK exits with the provider's reason plus a hint naming the key variables, --model and CLIFFY_AI_MODEL. Any other import error is re-raised instead of being reported as a missing extra.

  • ai generate validates before writing. The reply must be valid YAML, validate against CLIManifest, use the requested name, contain no includes, and generate a CLI that compiles as Python (nothing is executed). A rejected reply is sent back to the model with the reason, up to --retries times (default 2). If no valid manifest comes back, the command fails and writes nothing.

  • ai generate options. --force is now required to overwrite an existing file. CLI names are restricted to letters, digits, - and _.

  • ai ask is grounded in the installed package. Its instructions are built from the live click command tree, the annotated cli init template and CLIManifest.model_json_schema(). --cli accepts a manifest file or a loaded CLI.

  • Output. The answer goes to stdout and token usage to stderr. --preview prints the prompt and needs neither the extra nor a key. --max-tokens is passed through as model settings.

Registration (cliffy/cli.py, cliffy/run.py)

  • cli.py imports the ai group and registers it with cli.add_command(ai) (two lines; nothing else in the file changed).
  • run.py no longer imports cliffy.ai inside a bare except Exception, which silently dropped the whole ai group on any import error. This file is outside the stated scope, but the old block is what hid the pydantic-ai API break.

pyproject.toml

  • Removed the W291 per-file ignore for cliffy/ai.py; the prompts no longer have trailing whitespace.
  • Added one narrow pytest filterwarnings ignore for the '_UnionGenericAlias' is deprecated warning that google-genai raises at import on Python 3.14+.

Tests and docs

  • tests/test_ai.py now has 50 more tests (188 -> 238 in the suite), all offline, using pydantic-ai's FunctionModel and TestModel. They cover generate (write, fence stripping, output dir, retry with the validation error, giving up, --force, unsafe names, failed request, empty reply, --max-tokens), ask (answer, grounding, --cli with a file and a loaded CLI), missing extra, missing key, unknown model, missing provider SDK, and the no-import-at-startup guarantee.
  • docs/features.md: the AI section documents model selection, validation and retries, grounding, and stdout/stderr behaviour.

Breaking changes

  1. cli ai is always registered. Without the extra, the commands exit with an install hint; previously the group was silently missing.
  2. Default model changed from google:gemini-2.0-flash to the per-provider table above.
  3. ai generate no longer overwrites an existing <name>.yaml without --force.
  4. ai generate rejects CLI names containing anything other than letters, digits, - and _.
  5. Token usage is printed to stderr, not stdout.

How verified

macOS arm64, uv 0.13.0, uv sync --locked --all-extras, one environment per Python:

Python ruff format --check ruff check mypy cliffy --strict mypy tests pytest
3.11 pass pass pass pass 238 passed
3.15 pass pass pass pass 238 passed

Shell smoke tests on 3.15:

  • cli ai ask "hi" with no keys exits 1 with the missing-key message and hint.
  • cli ai generate demo "say hello" -m test exits 1 and writes nothing.
  • cli ai ask --preview "hi" prints the prompt.
  • import cliffy.cli leaves no pydantic_ai module in sys.modules.

No request was sent to a real provider, so the default model ids are checked against pydantic-ai's model list only.

🤖 Generated with Claude Code

Summary by Sourcery

Restore reliable offline-tested AI commands with lazy model loading, validated manifest generation, and grounded CLI assistance.

New Features:

  • Restore the cli ai generate and cli ai ask commands with provider-agnostic model selection and optional dependency handling.
  • Validate generated manifests before writing them, including schema conformance, requested names, self-containment, and generated Python syntax.
  • Ground ai ask responses in the installed cliffy command tree and manifest documentation, with support for manifest or loaded-CLI references.
  • Add prompt preview, retries, overwrite protection, safe CLI-name validation, token limits, and separated answer and usage output.

Bug Fixes:

  • Prevent AI import failures from silently removing the ai command group and provide actionable errors for missing extras, API keys, models, and provider SDKs.
  • Avoid writing invalid or failed AI-generated manifests to disk.

Enhancements:

  • Load pydantic-ai lazily so importing cliffy does not import the optional AI dependency.
  • Register the AI command group consistently in the main CLI and document model selection, validation, grounding, and output behavior.

Documentation:

  • Document the restored AI commands, model configuration, manifest validation and retries, CLI grounding, and stdout/stderr behavior.

Tests:

  • Expand AI coverage with offline FunctionModel and TestModel tests for command registration, lazy imports, model resolution, validation, retries, failures, previews, grounding, and output handling.

jaykv and others added 5 commits October 12, 2026 02:20
- pydantic-ai is only imported when a command talks to a model, so the ai
  group is always registered (in cliffy.cli) and a missing `ai` extra is
  reported with an install hint instead of the group silently vanishing.
- Model selection: --model, then CLIFFY_AI_MODEL, then a default for the
  first provider with an API key set. Unknown models and missing API keys
  exit with a readable error.
- ai generate validates the reply (YAML, CLIManifest, generated code
  compiles) and sends failures back to the model before writing anything.
  It no longer overwrites an existing manifest without --force.
- ai ask is grounded in the live command list, the cli init template and
  the manifest JSON schema instead of a hand-maintained copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Covers generate (valid, fenced, retry, exhausted retries, overwrite,
unsafe names, failed request), ask (grounding, --cli reference), model
selection, the missing-extra and missing-API-key paths, and that importing
cliffy does not import pydantic-ai.

Drops the W291 lint exemption cliffy/ai.py no longer needs and ignores a
DeprecationWarning google-genai raises at import on Python 3.14+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Revives cli ai generate and ask for pydantic-ai 2.x by lazily resolving provider models, validating and retrying generated manifests before atomic-style write decisions, grounding answers in the installed cliffy package, and registering the commands with actionable errors and comprehensive offline tests.

Sequence diagram for validated AI manifest generation

sequenceDiagram
    actor User
    participant CLI as cli ai generate
    participant Resolver as resolve_model
    participant Agent as pydantic_ai Agent
    participant Validator as validate_manifest_yaml
    participant File as Manifest file

    User->>CLI: generate description
    CLI->>Resolver: resolve_model(model)
    Resolver-->>CLI: provider model
    CLI->>Agent: generate_manifest(description)
    Agent->>Validator: output_validator(reply)
    alt reply rejected
        Validator-->>Agent: ModelRetry(reason)
        Agent->>Validator: corrected reply
    else manifest valid
        Validator-->>Agent: validated YAML
    end
    Agent-->>CLI: manifest and usage
    CLI->>File: write only after validation
    CLI-->>User: success and token usage
Loading

Flow diagram for AI model selection and error handling

flowchart TD
    Start["AI command"] --> Preview{"--preview?"}
    Preview -->|yes| Prompt["Print generated prompt"]
    Preview -->|no| Explicit{"--model or CLIFFY_AI_MODEL?"}
    Explicit -->|yes| Model["Use selected provider:model"]
    Explicit -->|no| Key{"Provider API key set?"}
    Key -->|yes| Default["Use first provider default"]
    Key -->|no| Fallback["Use google:gemini-flash-latest"]
    Model --> Extra["Require pydantic-ai extra"]
    Default --> Extra
    Fallback --> Extra
    Extra --> Error{"Model/provider usable?"}
    Error -->|no| Hint["Exit with provider reason and install/key hint"]
    Error -->|yes| Request["Send model request"]
Loading

File-Level Changes

Change Details Files
Rebuild the AI command implementation around lazy optional-dependency loading and provider-agnostic model resolution.
  • Import pydantic-ai only when a request is sent, preserving startup without the AI extra.
  • Resolve models from CLI option, environment override, or API-key-based provider defaults.
  • Convert missing extras, provider SDKs, API keys, and invalid models into actionable CLI errors while re-raising unrelated import failures.
  • Pass maximum-token settings through to pydantic-ai.
cliffy/ai.py
Add validated, retryable manifest generation that never writes an unusable result.
  • Strip common Markdown fences and validate YAML against CLIManifest.
  • Enforce the requested name, reject includes, generate the CLI, and compile generated Python without executing it.
  • Send validation failures back to the model for the configured retry count and write only after successful validation.
  • Require --force for overwrites and restrict generated CLI names to safe characters.
cliffy/ai.py
Ground AI answers in the installed cliffy implementation and separate answer output from usage reporting.
  • Build prompts from the live Click command tree, init template, manifest schema, and optional manifest or loaded-CLI reference.
  • Support offline prompt preview without importing or configuring pydantic-ai.
  • Write answers to stdout and token usage to stderr.
cliffy/ai.py
docs/features.md
Make the ai command group consistently available while keeping the dependency optional.
  • Register ai directly on the main CLI.
  • Remove the broad exception handler that previously hid AI import and registration failures.
  • Document the new optional-extra behavior and warning configuration.
cliffy/cli.py
cliffy/run.py
docs/features.md
pyproject.toml
Replace network-dependent AI tests with comprehensive offline model-driven coverage.
  • Use FunctionModel and TestModel to test model selection, errors, previews, generation validation and retries, safe file handling, grounding, output streams, and startup import behavior.
  • Add coverage for failed requests, empty responses, provider SDK errors, and max-token propagation.
tests/test_ai.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 84806a8a-56fa-43e3-a53b-427cb2d568a1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Needs a human reviewer. A generated manifest is written to disk and remains there if this change is reverted. The impact is bounded: the file can be inspected, edited, or deleted, and the CLI does not execute its generated code automatically.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@jaykv
jaykv changed the base branch from fix/audit-bugs-and-security to main October 12, 2026 21:22
@jaykv
jaykv merged commit 64d2d89 into main Oct 12, 2026
7 checks passed
@jaykv
jaykv deleted the feat/revive-ai-commands branch October 12, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant