Commit Graph

3 Commits

Author SHA1 Message Date
Jesús Mejías Leiva 350e0031f1
feat(ai): make the AI provider configurable (any laravel/ai provider, incl. local Ollama) (#718)
## Summary

Closes #716.

Both AI execution paths hard-coded `provider: Lab::Gemini`, so even
though `laravel/ai` already understands `OLLAMA_URL` and the model was
env-overridable, no other provider could ever be reached by the actual
UI features. This makes the **AI provider configurable**, keeping
**Gemini as the default** so existing deployments are unaffected.

Although issue #716 asked specifically for **Ollama**, the fix is
generic: because the provider is resolved through the
`Laravel\Ai\Enums\Lab` enum, this unlocks **any text provider
`laravel/ai` supports** — `gemini`, `openai`, `anthropic`, `azure`,
`groq`, `xai`, `deepseek`, `mistral`, and self-hosted `ollama`. Ollama
is the headline case (fully local, private processing), but nothing in
the code is Ollama-specific.

> Note: each provider still needs its own credentials configured for
`laravel/ai` (e.g. `GEMINI_API_KEY`, `OPENAI_API_KEY`, `OLLAMA_URL`),
and only text-capable providers apply — a non-text or unknown provider
fails fast.

## Changes

- **`config/ai_suggestions.php` / `config/ai_categorization.php`** — add
a `provider` key. Each reads its own `AI_SUGGESTIONS_PROVIDER` /
`AI_CATEGORIZATION_PROVIDER`, both falling back to a shared
`AI_PROVIDER` and finally `gemini`. So `AI_PROVIDER=<provider>` flips
every AI feature at once, and either feature can still be overridden
individually.
- **`app/Services/Ai/CategorizeTransactions.php` /
`app/Services/Ai/LaravelAiRuleSuggestionGenerator.php`** — resolve the
configured provider to the `Laravel\Ai\Enums\Lab` enum via
`Lab::from((string) config('...provider'))` and pass it to `prompt()`
(the SDK recommends referencing providers by the `Lab` enum rather than
a plain string). `Lab::from()` also **validates** the value: an unknown
provider fails fast with a clear `ValueError` instead of erroring deep
in the provider stack.
- **`.env.example`** — document the provider vars plus an Ollama block
(`OLLAMA_URL`, `OLLAMA_API_KEY`, `AI_CATEGORIZATION_MODEL`), kept
commented so defaults stay Gemini.
- **`README.md`** — new *AI Provider* section: the generic provider
switch, the list of supported text providers, and a local-Ollama
example.
- **Tests** — cover the `gemini` default and a non-Gemini (`ollama`)
override for both the categorization and rule-suggestion paths, plus the
fail-fast `ValueError` on an unknown provider.

## Usage

Any supported provider follows the same pattern — set `AI_PROVIDER`,
that provider's credentials, and the `*_MODEL` vars. Example, fully
local/private with Ollama:

```dotenv
AI_PROVIDER=ollama
OLLAMA_URL=http://ollama.example.local:11434
AI_SUGGESTIONS_MODEL=gemma3:12b
AI_CATEGORIZATION_MODEL=gemma3:12b
```

## Validation

- `./vendor/bin/pest tests/Feature/Ai` → **128 passed**.
- `vendor/bin/pint` and `vendor/bin/phpstan` (level 5) → clean.
- **End-to-end against a real Ollama server** (`gemma3:12b`), through
the actual application code (not faked):
  - Categorization: `MERCADONA COMPRA` → *Groceries*, confidence 0.95.
- Rule suggestion: `netflix` → *Subscriptions*, structured output
intact.

## Backward compatibility

Default provider is unchanged (`gemini`); no env changes are required
for existing installs.
2026-07-22 09:01:51 +02:00
Víctor Falcón d504e70309
fix(ai): don't report expected transient provider overloads (PHP-LARAVEL-44) (#655)
## Issue (Sentry PHP-LARAVEL-44 — low volume: 0 users, 1 event)

`Laravel\Ai\Exceptions\ProviderOverloadedException: AI provider [gemini]
is overloaded` (underlying Gemini HTTP 503 "high demand"), thrown from
the AI rule-suggestion generator. This is an expected, transient,
self-healing provider overload — but
`LaravelAiRuleSuggestionGenerator::generate()` called `report()` on
every failed batch, so a transient hiccup surfaced in Sentry as an
error.

## Fix

Split the per-batch catch so a `FailoverableException` (overload /
rate-limit / insufficient-credits) is logged at `warning` **without**
reporting, while every other `Throwable` is still `report()`ed. This
mirrors the existing, test-enforced handling in the sibling
`CategorizeTransactions` service.

Unchanged (deliberately preserved):
- **Partial tolerance** — suggestions from batches that succeeded are
still kept.
- **Rethrow-when-all-fail** — a genuine total failure still rethrows, so
the run is marked `Failed` (the onboarding "Try again / Skip" UI) and is
reported **once** at the run level. Non-transient batch errors are still
reported immediately.

## What I deliberately did NOT do

Two review agents converged that adding a **retry backoff** (the other
candidate fix) is not worth it here and carries real risk:
- The run **already self-heals**: failed/empty runs don't count toward
the throttle, so the user's "Try again" button re-runs immediately at
zero cost.
- A synchronous per-batch `sleep` on the single-worker `database` queue,
across up to ~10 batches, could push the run past its **120s job timeout
(failing more often, not less)** and starve other queued jobs during a
provider brownout.

So this PR is scoped to the safe, high-signal change: stop reporting an
expected transient as an error. Genuine misconfigurations (bad
model/key/quota) throw non-`FailoverableException` types and remain
fully reported.

## Testing

- New: an expected transient overload returns the successful batches'
suggestions and is **not** reported
(`Exceptions::assertNothingReported()`); an unexpected batch failure
**is** reported (`assertReported(RuntimeException::class)`).
- Full `tests/Feature/Ai` suite green (121 tests); Pint and Larastan
clean.

Fixes PHP-LARAVEL-44

---
🤖 Opened by the autonomous Sentry-triage loop. Auto-merge enabled:
low-risk, mirrors an existing tested pattern, keeps genuine failures
visible.
2026-07-07 10:55:02 +00:00
Víctor Falcón 8056ede636
feat(ai): suggest automation rules during onboarding (#523)
Suggests transaction categorization rules during onboarding.

After a sync or import, it groups the uncategorized transactions, asks
Gemini (via laravel/ai) to map the common merchants to categories, and
shows the results for review. The user edits or drops any and creates
the ones they want. During onboarding the accepted rules also categorize
existing transactions right away.

Off by default: it needs the `AiRuleSuggestions` Pennant flag and a
per-user AI consent. The model and thresholds are config-driven.
`ai:suggest-rules {user}` prints what a user would get.

The settings-page surface and monthly regeneration are a follow-up.
2026-06-13 22:51:15 +02:00