Files
InvoiceShelf/app/Providers/AiServiceProvider.php
Darko Gjorgjijoski 3df2a01832 feat(ai): add customer/item/expense ranking tools to the chat assistant
Adds three new read-only tools the chat LLM can call to answer
"who/what did the most X" questions that previously fell through
the cracks:

- rank_top_customers — ranks customers by invoiced_total, paid_total,
  invoice_count, or outstanding_balance over a named time period
- rank_top_items — ranks catalog items by quantity_sold or revenue
- rank_expense_categories — ranks expense categories by total spend

All three share a new ResolvesPeriod trait that centralizes the
period-name → [start, end] logic. GetCompanyStatsTool is refactored
onto the same trait (identical public schema — the 'all_time' option
is only exposed on the new ranking tools, where an unbounded window
makes sense; stats over "all time" collapses every record into one
giant bucket and is rarely useful).

Each tool follows the existing pattern: snake_case name, one-sentence
description tuned for LLM tool selection, JSON-schema parameters
with injected company scoping (never trusting LLM-supplied company
IDs), and JSON-encodable output. outstanding_balance on the customer
tool explicitly ignores the period param since it's a current-state
snapshot.

Multi-company scoping tests lock down the session-authoritative
boundary on every new tool. Per-metric ordering tests verify the
aggregate queries actually rank correctly, and an ad-hoc-item
exclusion test verifies rank_top_items skips invoice lines where
item_id is null (free-typed entries that have no catalog row to
rank by id).

15 new tests added (tests/Feature/Ai/Tools/); test suite grows from
398 to 413 passing. LLM tool count goes from 9 to 12 — the model
will discover the new tools automatically via the function-calling
schema with no prompt changes required.
2026-04-11 21:54:39 +02:00

57 lines
2.2 KiB
PHP

<?php
namespace App\Providers;
use App\Services\Ai\AiToolRegistry;
use App\Services\Ai\Tools\GetCompanyStatsTool;
use App\Services\Ai\Tools\GetCustomerTool;
use App\Services\Ai\Tools\GetInvoiceTool;
use App\Services\Ai\Tools\ListExpenseCategoriesTool;
use App\Services\Ai\Tools\ListOverdueInvoicesTool;
use App\Services\Ai\Tools\ListRecentPaymentsTool;
use App\Services\Ai\Tools\RankExpenseCategoriesTool;
use App\Services\Ai\Tools\RankTopCustomersTool;
use App\Services\Ai\Tools\RankTopItemsTool;
use App\Services\Ai\Tools\SearchCustomersTool;
use App\Services\Ai\Tools\SearchInvoicesTool;
use App\Services\Ai\Tools\SearchItemsTool;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\ServiceProvider;
/**
* Binds the AI tool registry as a singleton and registers every built-in
* read-only tool the chat assistant can call.
*
* Modules that ship additional tools should extend the registry from their
* own ServiceProvider::boot() by resolving AiToolRegistry from the container
* and calling register() on it.
*/
class AiServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(AiToolRegistry::class, function (Application $app): AiToolRegistry {
$registry = new AiToolRegistry;
// Built-in read-only tools (order is presentation-only; the LLM picks).
$registry->register(new SearchInvoicesTool);
$registry->register(new GetInvoiceTool);
$registry->register(new ListOverdueInvoicesTool);
$registry->register(new SearchCustomersTool);
$registry->register(new GetCustomerTool);
$registry->register(new ListRecentPaymentsTool);
$registry->register(new SearchItemsTool);
$registry->register(new ListExpenseCategoriesTool);
$registry->register(new GetCompanyStatsTool);
// Ranking tools — group-by aggregates the individual-record
// tools above can't express.
$registry->register(new RankTopCustomersTool);
$registry->register(new RankTopItemsTool);
$registry->register(new RankExpenseCategoriesTool);
return $registry;
});
}
}