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ément | Pris en charge |
|---|---|
| Java | 17 ou plus (maven.compiler.release 17 dans sdks/java/pom.xml) |
| Systèmes d'exploitation | Chaque plateforme avec un environnement Java 17 (Linux, macOS, Windows) |
| Dépendances | jackson-databind, jackson-datatype-jsr310, json-schema-validator (networknt), slf4j-api |
| Starter Spring Boot | enterprise-ai-spring, construit avec le BOM spring-boot-dependencies du POM parent |
| API du Control Plane | v1 |
Installation
-
Téléchargez le dépôt Maven depuis Harbor. Les paquets des SDK sont hébergés dans le projet
sdkdu registre Harbor de Navinspire (artefactsdk/java, tag = version du SDK). Le registre Navinspire estregistry.navinspire.ai; connectez-vous avec un compte robot qui peut lire (pull) le projetsdk(fourni par l'équipe plateforme). Si votre organisation réplique le projetsdkdans son propre Harbor, utilisez l'adresse de ce miroir.oras login registry.navinspire.aioras pull registry.navinspire.ai/sdk/java:1.0.2 -o sdk-javaRésultat attendu : le dossier
sdk-javacontient le dépôt Maven du SDK (io/enterpriseai/...). -
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(ougradle dependencies) listeio.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.
-
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.pommvn -o package -
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éthode | Méthode du builder | En-tête envoyé |
|---|---|---|
Clé API de tenant eai_<prefix>_<secret> | apiKey(String) | X-API-Key |
| Jeton d'accès OIDC | token(String) | Authorization: Bearer <token> |
Mode développement (serveur lancé avec EAI_AUTH_DEV=1), seulement sans identifiant | tenant(String) | X-Dev-Tenant |
| Identité de workload, mTLS | httpClient(HttpClient) avec un SSLContext qui porte le certificat client | certificat 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 builder | Type | Défaut | Description |
|---|---|---|---|
baseUrl | String | aucun, obligatoire | URL du Control Plane sans /v1 ; https sauf pour localhost, 127.0.0.1, ::1. |
apiKey | String | aucun | Clé API du tenant, envoyée dans X-API-Key. |
token | String | aucun | Jeton d'accès OIDC, envoyé dans Authorization: Bearer. |
tenant | String | aucun | Slug du tenant ; envoyé dans X-Dev-Tenant seulement sans identifiant. |
project | String | aucun | Envoyé dans X-Project. |
environment | String | aucun | dev, test, staging ou prod, envoyé dans X-Environment. |
locale | Locale | Locale.ENGLISH | Locale.ENGLISH ou Locale.FRENCH ; envoyé dans Accept-Language. |
retries | int | 2 | Reprises après la première tentative. |
retryBaseDelay | Duration | 250 ms | Premier délai d'attente, doublé à chaque tentative. |
timeout | Duration | 60 s | Délai d'une requête. |
connectTimeout | Duration | 10 s | Délai de connexion. |
caBundle | java.nio.file.Path | magasin du JDK | Fichier PEM d'autorités supplémentaires ; la vérification reste active. |
dangerouslyDisableTlsVerificationForLocalhost | aucun | désactivé | Désactive le contrôle des certificats ; build() le refuse sauf si l'hôte est local. |
httpClient | java.net.http.HttpClient | construit par le SDK | Client personnalisé (proxy, exécuteur) ; il ne doit pas suivre les redirections. |
traceparent | Supplier<String> | aléatoire par requête | Traceparent W3C du span courant. |
header | String, String | aucun | En-tête supplémentaire envoyé avec chaque requête. |
headerSupplier | Supplier<Map<String, String>> | aucun | En-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.
| Exception | Codes | Reprise par le SDK |
|---|---|---|
EaiException | tout 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 |
AuthException | unauthenticated, forbidden, ou HTTP 401 et 403 | non |
PolicyDeniedException | policy_denied | non |
QuotaExceededException | quota_exceeded, budget_exceeded | oui quand le statut est 429, puis levée |
EffortNotSupportedException | effort_not_supported | non |
ContextOverflowException | context_overflow | non |
ApprovalRequiredException | approval_required | non |
NotFoundException | not_found ou HTTP 404 | non |
EgressDeniedException | egress_denied | non |
PromptInjectionBlockedException | prompt_injection_blocked | non |
PayloadTooLargeException | payload_too_large | non |
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.xgarde chaque classe et méthode publique ; une suppression attend2.0. - Ils ciblent l'API
v1et les contratsv1; les résultatsJsonNodegardent 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
- Référence Java générée : chaque classe, méthode, paramètre et champ.
- API v1 du Control Plane, Contrats v1, guide temps réel.
- Autres SDK.