aquidify / sdk
PHP client for the Aquidify API, with a Laravel adapter: turn what people type into validated, structured intent.
Requires
- php: >=8.2
- ext-curl: *
- ext-json: *
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
- illuminate/support: Laravel adapter: config, auto-discovered service provider, Aquidify facade and Aquidify::fake() (^11.0|^12.0|^13.0)
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-09-23 09:46:54 UTC
README
Aquidify SDKs
Official clients for the Aquidify API: turn what people type into validated, structured intent your software can act on.
"Iščem delo v skladišču v Ljubljani, brez nočnih."
→ roles: warehouse · locations: Ljubljana · schedules: night (exclude)
Every extracted item quotes the exact span it came from (raw_text),
uncertainty is kept instead of guessed away, and "unknown" is never confused
with "any". Aquidify interprets; your application decides.
| Language | Folder | Install | Registry |
|---|---|---|---|
| PHP 8.2+ / Laravel | php/ |
composer require aquidify/sdk |
✓ published |
| TypeScript / JavaScript | js/ |
build from source: cd js && npm install && npm run build |
npm @aquidify/sdk, coming soon |
| Go | go/ |
go get github.com/aquidify/sdk/go@v0.1.0 |
✓ published |
| Python 3.9+ | python/ |
pip install "git+https://github.com/aquidify/sdk@v0.1.0#subdirectory=python" |
PyPI aquidify, coming soon |
| Java 17+ | java/ |
cd java && mvn install, then depend on com.aquidify:aquidify-sdk:0.1.0 |
Maven Central, coming soon |
| Rust | rust/ |
aquidify = { git = "https://github.com/aquidify/sdk", tag = "v0.1.0" } |
crates.io aquidify, coming soon |
| curl / any language | curl/ |
nothing to install | — |
Not listed? Generate a client from openapi.yaml, the source
of truth for every SDK here. Output shapes per domain are JSON Schemas in
schemas/.
All clients read AQUIDIFY_API_KEY from the environment when no key is passed.
Keep keys server-side: never ship one to a browser or mobile app.
PHP
composer require aquidify/sdk
Laravel discovers the service provider and facade on its own; publish the
config with php artisan vendor:publish --tag=aquidify-config when you want to
change the base URL or timeout.
$aq = new Aquidify\Client(); // AQUIDIFY_API_KEY $r = $aq->interpret('hiring.candidate', 'Iščem delo v skladišču v Ljubljani, brez nočnih.', 'sl-SI'); $r['interpretation']['intents'][0]['roles'][0]['value']; // "warehouse" $r['clarification']; // null
Your own task, for any industry (see curl/README.md):
$aq->putTask('support.ticket@1.0.0', [ 'instructions' => 'Classify customer support emails for routing.', 'schema' => ['type' => 'object', 'properties' => [ 'category' => ['enum' => ['billing', 'bug', 'refund', 'other']], 'order_id' => ['type' => 'string', 'description' => 'Order number, digits only'], ]], ]); $fields = $aq->interpret('support.ticket@1.0.0', $emailBody, 'en')['interpretation']['fields'];
Every SDK can interpret with a task id as the domain. Registering tasks
(putTask / getTask / listTasks) is in the PHP, TypeScript, Go and Python
SDKs (put_task … in Python); from Java and Rust use plain HTTP for now.
Laravel
The PHP package includes a Laravel adapter (auto-discovered): config, a
service provider, the Aquidify facade and a fake for tests.
# .env AQUIDIFY_API_KEY=... AQUIDIFY_TIMEOUT=8 # seconds; keep short in web requests and fall back AQUIDIFY_PARSER_VERSION=1.0.0 # pinned by default AQUIDIFY_SCHEMA_VERSION=1.0.0 php artisan vendor:publish --tag=aquidify-config # optional
use Aquidify\Interpreter; use Aquidify\Laravel\Facades\Aquidify; // inject… public function __construct(private Interpreter $aquidify) {} $r = $this->aquidify->interpret('hiring.candidate', $text, app()->getLocale()); // …or use the facade $r = Aquidify::interpret('hiring.candidate', $text, 'sl');
Tests never touch the network:
use Aquidify\Testing\FakeClient; Aquidify::fake([ 'skladišče' => FakeClient::interpretation(['intents' => [/* … */]]), // input substring => response '*' => FakeClient::error('model_unavailable'), // everything else fails ]); // … exercise your code … Aquidify::assertInterpreted(fn ($call) => $call['locale'] === 'sl');
Aquidify::fake() replaces both the facade and every injected Interpreter.
Without Laravel, use new FakeClient([...]) directly.
TypeScript / JavaScript
import { Aquidify, type HiringCandidateInterpretation } from "@aquidify/sdk"; const aq = new Aquidify(); const r = await aq.interpret<HiringCandidateInterpretation>({ domain: "hiring.candidate", input: "Iščem delo v skladišču v Ljubljani, brez nočnih.", locale: "sl-SI", }); r.interpretation.intents[0].schedules[0]; // { value: "night", polarity: "exclude", … }
Go
c, _ := aquidify.New("") r, err := c.Interpret(ctx, aquidify.Request{Domain: "hiring.candidate", Input: "…", Locale: "sl-SI"}) // r.Interpretation is json.RawMessage: unmarshal into your own types
Python
from aquidify import Aquidify r = Aquidify().interpret("hiring.candidate", "Iščem delo v skladišču v Ljubljani, brez nočnih.", "sl-SI") r["interpretation"]["intents"][0]["locations"][0]["value"] # "Ljubljana"
Java
var r = new Aquidify(null).interpret("hiring.candidate", "Iščem delo v skladišču v Ljubljani, brez nočnih.", "sl-SI"); r.interpretation().at("/intents/0/roles/0/value").asText(); // "warehouse"
Rust
let r = aquidify::Client::from_env()? .interpret(&aquidify::Request::new("hiring.candidate", "Iščem delo …", "sl-SI"))?; r.interpretation["intents"][0]["roles"][0]["value"]; // "warehouse"
Search engines: Meilisearch, Algolia, Elasticsearch
Aquidify reads the query; your search engine finds and ranks the documents. Filters (PHP) and
SearchFilters (TypeScript) turn an answer into that engine's filter. You map interpretation fields
to your index attributes; unmapped fields are ignored.
use Aquidify\Search\Filters; $r = $aq->interpret('hiring.candidate', 'Warehouse job in Maribor, mornings only, no weekends', 'en'); $f = Filters::from($r, ['roles' => 'category', 'locations' => 'city', 'schedules' => 'shift', 'salary' => 'salary']); $meili->index('jobs')->search('', ['filter' => $f->meilisearch()]); // category = "warehouse" AND city = "Maribor" AND shift = "morning" AND shift NOT IN ["weekend"] $algolia->searchSingleIndex('jobs', ['filters' => $f->algolia(), 'optionalFilters' => $f->algoliaOptional()]); $es->search(['index' => 'jobs', 'body' => ['query' => $f->elasticsearch()]]); // OpenSearch: same DSL
import { SearchFilters } from "@aquidify/sdk"; const f = SearchFilters.from(r, { roles: "category", locations: { attribute: "city", value: (v) => v.toLowerCase() } }); await index.search("", { filter: f.meilisearch() });
| In the answer | Becomes |
|---|---|
include + required |
a filter; several values for one attribute are OR-ed |
acceptable / conditional |
widens that filter, never filters on its own |
preferred |
a boost only: Algolia optionalFilters, Elasticsearch should (Meilisearch: none) |
exclude + required |
NOT |
{min, max} (salary, price) |
a numeric range |
| "any", or not said | nothing: no filter |
One sentence can describe several searches (hiring intents). Meilisearch and Elasticsearch get them
OR-ed; Algolia cannot OR groups, so run searches() as a multi-query. For Elasticsearch, map text
fields to their keyword sub-field (city.keyword). Translate values into your index's vocabulary with
['attribute' => 'city', 'value' => fn ($v) => ...].
Shops: WooCommerce and Shopify (PHP)
The same Filters turns a shop task's answer into the store's own product search. A
shop's attributes arrive as "colour: red", "size: 42"; by_name sends each name to
its own filter, and names you did not map are ignored, not guessed.
// WooCommerce: wc_get_products() arguments $f = Filters::from($response, [ 'products' => 's', 'attributes' => ['by_name' => ['colour' => 'pa_color', 'size' => 'pa_size']], 'brands' => 'product_brand', // core since WooCommerce 9.6 'price' => '_price', ]); wc_get_products($f->woocommerce()); // Shopify: Storefront API search(query:, productFilters:, types: [PRODUCT]) $f = Filters::from($response, [ 'products' => 'query', 'attributes' => ['by_name' => ['colour' => 'option:Color', 'size' => 'option:Size']], 'brands' => 'vendor', 'price' => 'price', ]); $variables = $f->shopify(); $f->shopifyLeftOut(); // e.g. ["vendor != nike"]
"Red running shoes, size 42, up to €80, just not Nike." becomes a search for running
shoes in red, size 42, at most 80, with Nike excluded (WooCommerce NOT IN). Shopify's
product filters cannot exclude, so the exclusion is reported by shopifyLeftOut()
instead of being dropped silently. Prices are compared as numbers in the store's
currency; convert or drop a price in another currency first.
Errors
Every client raises one error type carrying the HTTP status, the API's
stable code (unauthorized, rate_limited, invalid_request,
model_unavailable, …, or network), the request_id and retry_after, plus
a retryable check. The full table is in curl/. Clients do not
retry on their own: retry retryable errors after retry_after, ideally with
an Idempotency-Key.
Versions
Responses carry parser_version and schema_version. Pin both in production
to keep the shape stable while newer parsers roll out. A released schema version
never changes; changes ship as new versions.
Testing the SDKs
Each SDK has a live smoke test that makes no model calls (it costs nothing):
export AQUIDIFY_API_KEY=... composer install && composer test # PHP + Laravel adapter cd js && npm install && npm test # TypeScript cd go && go test ./... # Go cd python && uv run python tests/smoke.py # Python cd java && mvn -q test # Java cd rust && cargo test # Rust
License
MIT