Aller au contenu principal

SDK Java

Ce guide montre comment installer le SDK Java (io.enterpriseai:enterprise-ai-java), le connecter au Control Plane de la Navin Platform et appeler chaque groupe de ressources de l'API v1 avec des exceptions typées, des reprises, la pagination et le streaming. Source : sdks/java/enterprise-ai-java/src/main/java/io/enterpriseai. Chaque classe, méthode et champ figure dans la référence générée.

English version: java.md

Vue d'ensemble​

EnterpriseAIClient est un client sûr entre threads sur java.net.http.HttpClient. Il se construit avec EnterpriseAIClient.builder() et expose un accesseur par groupe de ressources de l'API v1 (client.agents(), client.gateway(), client.finops()...). Chaque méthode correspond à une route documentée et renvoie un JsonNode Jackson, un modèle typé (ChatResponse, ChatChunk) ou une Page<JsonNode>. Les erreurs sont des sous-classes non vérifiées de EaiException. L'artefact contient aussi io.enterpriseai.manager.ManagerClient pour le Navin Manager (SDK du Navin Manager) et la validation hors ligne des contrats (io.enterpriseai.contracts.ContractValidator). L'artefact séparé io.enterpriseai:enterprise-ai-spring est un starter Spring Boot.

Prérequis et compatibilité​

ÉlémentPris en charge
Java17 ou plus (maven.compiler.release 17 dans sdks/java/pom.xml)
Systèmes d'exploitationChaque plateforme avec un environnement Java 17 (Linux, macOS, Windows)
Dépendancesjackson-databind, jackson-datatype-jsr310, json-schema-validator (networknt), slf4j-api
Starter Spring Bootenterprise-ai-spring, construit avec le BOM spring-boot-dependencies du POM parent
API du Control Planev1

Installation​

  1. Téléchargez le dépôt Maven depuis Harbor. Les paquets des SDK sont hébergés dans le projet sdk du registre Harbor de Navinspire (artefact sdk/java, tag = version du SDK). Le registre Navinspire est registry.navinspire.ai ; connectez-vous avec un compte robot qui peut lire (pull) le projet sdk (fourni par l'équipe plateforme). Si votre organisation réplique le projet sdk dans son propre Harbor, utilisez l'adresse de ce miroir.

    oras login registry.navinspire.ai
    oras pull registry.navinspire.ai/sdk/java:1.0.2 -o sdk-java

    Résultat attendu : le dossier sdk-java contient le dépôt Maven du SDK (io/enterpriseai/...).

  2. Déclarez ce dossier comme dépôt, puis ajoutez la dépendance. Maven :

    <repositories>
    <repository>
    <id>enterprise-ai-sdks</id>
    <url>file://${project.basedir}/sdk-java</url>
    </repository>
    </repositories>
    <dependency>
    <groupId>io.enterpriseai</groupId>
    <artifactId>enterprise-ai-java</artifactId>
    <version>1.0.2</version>
    </dependency>

    Gradle (Kotlin DSL) :

    repositories {
    maven { url = uri("sdk-java") }
    }
    dependencies {
    implementation("io.enterpriseai:enterprise-ai-java:1.0.2")
    }

    Résultat attendu : mvn dependency:tree (ou gradle dependencies) liste io.enterpriseai:enterprise-ai-java:1.0.2.

    En cas d'échec : vérifiez java -version (17 ou plus) et les dépôts de la construction.

Installation hors ligne​

Le dépôt Maven récupéré dans Harbor est un dossier local : une construction qui résout depuis ce dossier n'a besoin d'aucun autre dépôt pour le SDK lui-même.

  1. Ou installez le jar copié avec la version dans le dépôt local, puis construisez hors ligne (-o).

    mvn install:install-file -Dfile=enterprise-ai-java-1.0.2.jar -DpomFile=enterprise-ai-java-1.0.2.pom
    mvn -o package
  2. Ou construisez depuis ce dépôt.

    cd sdks/java && mvn -q install

Démarrage rapide​

Cet exemple construit un client, lit l'identité de l'appelant et envoie une complétion de chat par le gateway.

EnterpriseAIClient quick = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.build();
JsonNode identity = quick.meta().me();
ChatResponse reply = quick.gateway().chatCompletions(ChatRequest.builder("gpt-4o-mini").user("Bonjour").build());
System.out.println(identity.path("id").asText() + " " + reply.content());

Ce qui se passe : build() vérifie l'URL de base (https obligatoire sauf sur localhost), chaque appel envoie la clé dans X-API-Key et un traceparent W3C, et la complétion part avec une Idempotency-Key. Résultat attendu : l'identifiant de l'appelant, puis le texte de la réponse.

Authentification​

Le serveur résout le tenant à partir de l'identifiant. Le SDK ne lit aucune variable d'environnement : passez les valeurs au builder, par exemple avec System.getenv.

MéthodeMéthode du builderEn-tête envoyé
Clé API de tenant eai_<prefix>_<secret>apiKey(String)X-API-Key
Jeton d'accès OIDCtoken(String)Authorization: Bearer <token>
Mode développement (serveur lancé avec EAI_AUTH_DEV=1), seulement sans identifianttenant(String)X-Dev-Tenant
Identité de workload, mTLShttpClient(HttpClient) avec un SSLContext qui porte le certificat clientcertificat client TLS
EnterpriseAIClient client = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.apiKey(System.getenv("EAI_API_KEY"))
.tenant("acme")
.project("invoices")
.locale(Locale.ENGLISH)
.build();

Configuration du client​

EnterpriseAIClient.builder() renvoie un EnterpriseAIClient.Builder :

Méthode du builderTypeDéfautDescription
baseUrlStringaucun, obligatoireURL du Control Plane sans /v1 ; https sauf pour localhost, 127.0.0.1, ::1.
apiKeyStringaucunClé API du tenant, envoyée dans X-API-Key.
tokenStringaucunJeton d'accès OIDC, envoyé dans Authorization: Bearer.
tenantStringaucunSlug du tenant ; envoyé dans X-Dev-Tenant seulement sans identifiant.
projectStringaucunEnvoyé dans X-Project.
environmentStringaucundev, test, staging ou prod, envoyé dans X-Environment.
localeLocaleLocale.ENGLISHLocale.ENGLISH ou Locale.FRENCH ; envoyé dans Accept-Language.
retriesint2Reprises après la première tentative.
retryBaseDelayDuration250 msPremier délai d'attente, doublé à chaque tentative.
timeoutDuration60 sDélai d'une requête.
connectTimeoutDuration10 sDélai de connexion.
caBundlejava.nio.file.Pathmagasin du JDKFichier PEM d'autorités supplémentaires ; la vérification reste active.
dangerouslyDisableTlsVerificationForLocalhostaucundésactivéDésactive le contrôle des certificats ; build() le refuse sauf si l'hôte est local.
httpClientjava.net.http.HttpClientconstruit par le SDKClient personnalisé (proxy, exécuteur) ; il ne doit pas suivre les redirections.
traceparentSupplier<String>aléatoire par requêteTraceparent W3C du span courant.
headerString, StringaucunEn-tête supplémentaire envoyé avec chaque requête.
headerSupplierSupplier<Map<String, String>>aucunEn-têtes calculés à chaque requête (ils remplacent project et environment).

Options par appel : RequestOptions.create().idempotencyKey(k).traceparent(tp).header(name, value), acceptées par les méthodes qui ont une surcharge avec RequestOptions.

EnterpriseAIClient tuned = EnterpriseAIClient.builder()
.baseUrl("<control-plane-url>")
.token(System.getenv("EAI_TOKEN"))
.environment("prod")
.retries(4)
.retryBaseDelay(Duration.ofMillis(500))
.timeout(Duration.ofSeconds(30))
.connectTimeout(Duration.ofSeconds(5))
.header("X-Client", "billing")
.build();
JsonNode created = tuned.agents().create(Map.of("kind", "Agent"), RequestOptions.create().idempotencyKey("agent-ap-1"));

Builder.toString() masque la clé API et le jeton.

Requêtes et réponses​

Les corps sont toute valeur sérialisable par Jackson (Map, record, POJO) ; les résultats sont des JsonNode (lus avec path(...)), ou des modèles typés pour le gateway : ChatResponse (content(), usage(), decisionId(), costEstimated(), costMeasured()) et ChatChunk (delta()). client.transport().exchange(...) renvoie le statut et les en-têtes de n'importe quel appel.

ChatResponse r = client.gateway().chatCompletions(ChatRequest.builder("gpt-4o-mini")
.user("Bonjour")
.effort(Effort.LOW).agent("agt_ap", null, null, null)
.build());
System.out.println(r.costMeasured() + " " + r.decisionId());
try (ChatStream s = client.gateway().chatCompletionsStream(req)) { s.stream().forEach(c -> System.out.print(c.delta())); }

Erreurs​

Chaque réponse d'erreur utilise l'enveloppe {"error": {"code", "message", "message_i18n", "details", "request_id"}}. Le SDK lève une EaiException non vérifiée (ou une sous-classe) avec status() (0 pour une erreur réseau), code(), getMessage() (dans la langue du client), details() (clés d'allure secrète masquées), requestId() et retryAfterSeconds(). Sans enveloppe, le code vient du statut : 400 et 422 validation_error, 401 unauthenticated, 403 forbidden, 404 not_found, 409 conflict, 429 rate_limited, 5xx server_error.

ExceptionCodesReprise par le SDK
EaiExceptiontout autre code (validation_error, conflict, rate_limited, server_error, network_error...)rate_limited (429), 408, 502, 503, 504 et network_error oui, puis levée
AuthExceptionunauthenticated, forbidden, ou HTTP 401 et 403non
PolicyDeniedExceptionpolicy_deniednon
QuotaExceededExceptionquota_exceeded, budget_exceededoui quand le statut est 429, puis levée
EffortNotSupportedExceptioneffort_not_supportednon
ContextOverflowExceptioncontext_overflownon
ApprovalRequiredExceptionapproval_requirednon
NotFoundExceptionnot_found ou HTTP 404non
EgressDeniedExceptionegress_deniednon
PromptInjectionBlockedExceptionprompt_injection_blockednon
PayloadTooLargeExceptionpayload_too_largenon
try { client.gateway().chatCompletions(req); }
catch (QuotaExceededException e) { log.warn("{} {} {}", e.code(), e.getMessage(), e.requestId()); }
client.runs().list(Map.of("limit", 50)).autoPaging().forEach(r -> System.out.println(r.path("id").asText()));

Reprises et délais​

Une erreur d'entrée-sortie et les statuts HTTP 408, 429, 502, 503 et 504 sont repris jusqu'à retries fois (2 par défaut), pour toutes les méthodes : un POST garde son Idempotency-Key entre les reprises, donc le serveur le dédoublonne. Le délai commence à retryBaseDelay (250 ms), double à chaque tentative jusqu'à 20 s et ajoute jusqu'à 20 % d'aléa ; un en-tête Retry-After l'emporte, plafonné à 60 s. timeout (60 s) borne chaque requête et connectTimeout (10 s) chaque connexion. Un thread interrompu s'arrête aussitôt avec un network_error.

Pagination​

Les méthodes de liste renvoient une Page<JsonNode> : items(), nextCursor(), hasNext(), next() (la page suivante), autoPaging() (un Iterable sur chaque élément à travers les pages) et stream() (un Stream sur chaque élément).

Page<JsonNode> first = client.agents().list(Map.of("limit", 100));
System.out.println(first.items().size() + " " + first.hasNext());
long active = first.stream().filter(a -> "active".equals(a.path("status").asText())).count();

Idempotence et opérations longues​

Chaque POST envoie une Idempotency-Key : la valeur de RequestOptions.idempotencyKey(...), sinon un UUID généré et conservé entre les reprises de l'appel. Une création longue répond avec les en-têtes Operation-Id et Operation-Location ; client.operations().get(id) lit l'opération, sa ressource suivie et son état courant.

JsonNode run = client.runs().start("agt_ap", Map.of("task", Map.of("invoice_media_id", "med_01HZX3K9")), RequestOptions.create().idempotencyKey("inv-2291"));
JsonNode done = client.runs().getRaw(run.path("id").asText());
JsonNode operation = client.operations().get("op_1");
System.out.println(operation.path("status").asText());

Asynchrone et concurrence​

Le client est sûr entre threads : partagez une seule instance. Les appels sont bloquants ; exécutez-les sur un exécuteur (par exemple des threads virtuels en Java 21) pour la concurrence. SseStream.publisher(executor) expose un flux comme un java.util.concurrent.Flow.Publisher, et transport().websocket(...) renvoie un CompletableFuture<WebSocket>.

var pool = java.util.concurrent.Executors.newFixedThreadPool(4);
var meta = java.util.concurrent.CompletableFuture.supplyAsync(() -> client.meta().get(), pool);
var models = java.util.concurrent.CompletableFuture.supplyAsync(() -> client.models().list(), pool);
System.out.println(meta.join().path("version").asText() + " " + models.join().items().size());
pool.shutdown();

Streaming et temps réel​

gateway().chatCompletionsStream(req) renvoie un ChatStream (un Iterator<ChatChunk> et AutoCloseable avec stream()) ; fermez-le avec try-with-resources. Les sessions vocales en direct utilisent client.realtime() et le WebSocket du transport : voir le guide temps réel.

Journalisation et débogage​

Le SDK journalise par SLF4J : le logger io.enterpriseai.Transport écrit une ligne DEBUG par reprise (méthode, chemin, statut ou erreur d'entrée-sortie). Liez SLF4J à votre moteur de journalisation (Logback, Log4j 2) et réglez le niveau de ce logger. Les identifiants ne sont jamais journalisés.

<logger name="io.enterpriseai.Transport" level="DEBUG"/>

Donnez le requestId() d'une exception à l'équipe plateforme : c'est la clé de l'événement d'audit et de la trace.

Tests​

Dirigez le client vers un serveur HTTP local : le vrai transport s'exécute (en-têtes, reprises, exceptions). Le serveur du JDK com.sun.net.httpserver.HttpServer ne demande aucune dépendance ; les tests du SDK utilisent MockWebServer d'OkHttp.

com.sun.net.httpserver.HttpServer server = com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/v1/agents", exchange -> {
byte[] body = "{\"items\":[{\"id\":\"agt_1\"}],\"next_cursor\":null}".getBytes(java.nio.charset.StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
exchange.close();
});
server.start();
EnterpriseAIClient testClient = EnterpriseAIClient.builder()
.baseUrl("http://127.0.0.1:" + server.getAddress().getPort())
.apiKey("eai_test_key")
.retries(0)
.build();
System.out.println(testClient.agents().list().items().size());
server.stop(0);

Versions et compatibilité​

  • Les artefacts suivent le versionnage sémantique : 1.x garde chaque classe et méthode publique ; une suppression attend 2.0.
  • Ils ciblent l'API v1 et les contrats v1 ; les résultats JsonNode gardent les champs qu'un serveur plus récent ajoute.
  • Fixez la version dans la construction et lisez les notes de version avant une mise à jour.

Guides des ressources​

Une sous-section par groupe de ressources, avec un exemple par appel courant. Chaque méthode est dans la référence générée.

Outils et connecteurs​

tools(), mcp(), mcpRuntime(), skills(), plugins(), connectors(), credentials(), webhooks(), channels(), navinLibrary(), fileSecurity().

client.tools().invoke("erp.lookup", Map.of("arguments", Map.of("invoice", "INV-2291")));
client.connectors().test("sharepoint");

Multimodal​

media(), multimodal(), realtime().

JsonNode m = client.multimodal().vision(Map.of("model", "vision-default", "media_id", "med_IMG1", "prompt", "Total ?"));
client.multimodal().stt(Map.of("media_id", "med_AUD1", "language", "fr"));

Qualité et audit​

evaluations(), redteam(), audit(), telemetry().

JsonNode g = client.evaluations().checkGate(Map.of("target", Map.of("type", "agent", "ref", "agt_ap", "version", "1.4.0"), "suite_ids", List.of("evs_12")));
boolean ok = client.audit().verify().path("valid").asBoolean();

FinOps et quotas​

finops(), quotas(). Une Reservation est AutoCloseable : close() libère une réservation non confirmée.

JsonNode e = client.finops().estimate(Map.of("provider", "openai", "usage_type", "llm", "model_id", "gpt-4o-mini", "units", Map.of("input_tokens", 12000, "output_tokens", 2000)));
try (Reservation r = client.quotas().reserve(Map.of("cost", "0.50", "scope", Map.of("agent", "agt_ap")))) {
ChatResponse out = client.gateway().chatCompletions(req);
r.commit(Map.of("cost", out.costMeasured()));
} // close() releases an uncommitted reservation

Gouvernance, SOC, stockage, place de marché et preuves​

license(), telemetry(), identity(), context(), delegations(), breakGlass(), soc(), storage(), marketplace(), models(), policies(), evidence(), realtime().

JsonNode lic = client.license().get(); // GET /v1/license
JsonNode slo = client.telemetry().slo(Map.of("window_minutes", 60)); // GET /v1/telemetry/slo
JsonNode me = client.identity().whoami(); // GET /v1/identity/whoami
JsonNode ctx = client.context().issue(Map.of("purpose", "support"));
client.delegations().restrict("dlg_1", Map.of("scopes", List.of("read")));
client.breakGlass().request(Map.of("reason", "incident INC-42", "ttl_seconds", 900));
client.soc().createOrder(Map.of("incident_id", "inc_1", "action", "ISOLATE_NETWORK"));
client.soc().webhookSigned("falco", payload, secret, "acme"); // HMAC X-EAI-Signature
client.storage().presign("stp_1", Map.of("key", "exports/a.json", "scope", "read"));
client.marketplace().preflight(Map.of("package", "crm-agent", "version", "1.2.0"));
client.models().startRollout("mdl_1", "3", Map.of("percent", 10));
byte[] tgz = client.policies().downloadBundle("current"); // signed OPA bundle
client.evidence().verify();
client.realtime().turns("rts_1");

Webhooks, heartbeats et usage​

webhooks(), heartbeats(), quotas() ; TokenUnits.of(...) construit les unités de jetons d'un événement d'usage.

Map<String, String> h = WebhooksClient.signedHeaders(raw, secret, "acme", "evt-42", Instant.now().getEpochSecond());
client.webhooks().deliverSigned("sap", raw, secret, "acme");
Map<String, Object> units = TokenUnits.of(1200, 300, 800, 120);
if (HeartbeatsClient.shouldStop(client.heartbeats().beat(runId, Map.of("progress", 0.5)))) { /* checkpoint, exit */ }
try (Reservation r = client.quotas().reserve(Map.of("cost", "0.50", "tokens", 4000))) {
r.commit(Map.of("cost", "0.12", "tokens", 1800)); // r.id() is the "id" field
}

Interfaces de l'architecture​

SdkInterfaces.of(client) regroupe les interfaces nommées de l'architecture. Un refus est levé avec son exception typée (relation_required, approval_closed, storage_object_protected), et une réponse qui ne confirme pas le résultat est levée, jamais renvoyée comme un succès.

SdkInterfaces api = SdkInterfaces.of(client);
try {
JsonNode assumed = api.identity().assumeService("si_billing", "invoice sync", List.of("erp"), 600, null);
String serviceToken = assumed.path("token").asText();
} catch (EaiException e) {
if ("relation_required".equals(e.code())) { /* write the can_assume relation first */ }
}
client.audit().record(Map.of("action", "app.invoice.validated", "resource", Map.of("type", "invoice", "id", "inv-42"), "outcome", "success"));
api.approval().cancel("apr_1", "request withdrawn");
JsonNode objects = client.storage().listObjects("stp_1", Map.of("prefix", "exports/", "limit", 100));
api.storage().delete("stp_1", "exports/old.json", null);

Espace Platform Manager (platform_admin)​

platformManager() atteint l'espace Platform Manager de la console, relayé par le Control Plane au nom de l'appelant.

JsonNode summary = client.platformManager().summary(); // GET /v1/platform-manager/summary
JsonNode services = client.platformManager().services("c1"); // five state fields and administrative state
JsonNode op = client.platformManager().requestOperation("c1", "opa", Map.of("action", "rolling_restart", "batch_size", 1));
JsonNode restore = client.platformManager().planRestore(Map.of("artifact_id", "b1", "environment", "isolated")); // ADR-0162
JsonNode exercises = client.platformManager().drExercises(); // observed RPO and RTO

Spring Boot​

Le starter Spring Boot 3 enterprise-ai-spring crée un bean EnterpriseAIClient à partir des propriétés enterprise-ai.* (base-url, api-key, token, tenant, project, environment, locale par défaut en, retries par défaut 2, timeout par défaut 60 s, tool-owner, interceptor par défaut true) et, quand enterprise-ai.manager.base-url est renseigné, un bean ManagerClient à partir de enterprise-ai.manager.* (base-url, token, token-file, ca-bundle, on-behalf-of, locale, retries, timeout par défaut 30 s).

enterprise-ai:
base-url: <control-plane-url>
api-key: ${EAI_API_KEY}
tenant: acme
locale: fr

Client du Navin Manager​

io.enterpriseai.manager.ManagerClient appelle directement l'API du Navin Manager avec un jeton du manager nmt_<12 hex>_<secret> ; GET, PUT et DELETE sont repris, POST et PATCH jamais. Le guide complet est SDK du Navin Manager.

io.enterpriseai.manager.ManagerClient nm = io.enterpriseai.manager.ManagerClient.builder()
.baseUrl("<manager-url>")
.token(System.getenv("NM_MANAGER_TOKEN"))
.onBehalfOf("alice")
.build();
JsonNode action = nm.services().action("c1", "kafka", Map.of("action", "restart"));

Référence​