1. Witamy, deweloperze Gemini!

Z tego ćwiczenia dowiesz się, jak tworzyć aplikacje AI nowej generacji w języku Java za pomocą niestandardowego pakietu Gemini Interactions SDK.
Czym jest interfejs Gemini Interactions API?
Tradycyjne interfejsy LLM API są bezstanowe i działają na zasadzie żądanie-odpowiedź. Aby stworzyć wieloetapowego asystenta czatu lub złożoną pętlę agenta, programiści musieli dotychczas zarządzać stanem rozmowy, obcinaniem historii, orkiestracją wywołań narzędzi i pętlami wykonywania w kodzie aplikacji po stronie klienta.
Gemini Interactions API przenosi tę złożoność na serwer. Jest to interfejs API oparty na sesjach i stanach, w którym infrastruktura Google hostuje i zarządza grafem konwersacji. Pojedyncza interakcja reprezentuje sesję stanową. Gdy wchodzisz z nim w interakcję, interfejs API zwraca bogatą, uporządkowaną oś czasu składającą się z polimorficznych kroków, takich jak:
ThoughtStep: wewnętrzny proces rozumowania modelu.ModelOutputStep: tekst, dźwięk lub obraz wygenerowany przez model.ToolCallStepiToolResultStep: wywołania narzędzi zainicjowane przez system lub model.UserInteractionStep: miejsca, w których system wstrzymuje działanie, aby poprosić o interwencję lub zatwierdzenie przez człowieka.
Czym są zarządzani pracownicy obsługi klienta?
Orkiestracja autonomicznych agentów – obsługa pętli, logika ponawiania, środowiska wykonywania narzędzi i zarządzanie stanem – jest niezwykle trudna.
Zarządzane agenty to rozwiązanie na poziomie platformy udostępniane przez interfejs Gemini Interactions API. Zamiast uruchamiać pętle agentów lokalnie, możesz udostępniać wyspecjalizowanych agentów bezpośrednio w infrastrukturze Google:
- Wbudowane agenty: gotowe do użycia wyspecjalizowane agenty, takie jak agent Deep Research, który przeprowadza wieloetapowe badania w internecie, zbiera wyniki i generuje kompleksowe raporty.
- Niestandardowi zarządzani agenci: autonomiczne podmioty zdefiniowane przez Ciebie. Podajesz instrukcje systemowe, dołączasz narzędzia (takie jak wyszukiwarka Google lub środowisko wykonawcze Bash) i konfigurujesz piaskownicę w chmurze – bezpieczne, odizolowane i skonteneryzowane środowisko wykonawcze z konfigurowalnymi regułami wychodzącymi sieci (np. zezwalającymi na dostęp tylko do określonych domen, takich jak GitHub).
Korzystając z pakietu Gemini Interactions Java SDK, możesz łatwo uruchamiać te zarządzane agenty, koordynować ich działania i współpracować z nimi w standardowych aplikacjach Java.
Czego się nauczysz
- Jak poruszać się po nowej architekturze polimorficznej opartej na
Step. - Jak przesyłać strumieniowo ekspresywny dźwięk TTS bezpośrednio do głośników.
- Jak generować muzykę (MP3 + tekst) za pomocą Lyrii.
- Jak generować wizualne notatki graficzne za pomocą Gemini 3 Pro Image.
- Jak kierować agentem Deep Research za pomocą funkcji Collaborative Planning.
- Jak udostępnić agenta niestandardowego z regułami i narzędziami dotyczącymi ruchu wychodzącego z sieci.
Czego potrzebujesz
- Java 21 lub nowsza.
- Apache Maven.
- Edytor tekstu lub IDE (IntelliJ IDEA, VS Code itp.).
- Klucz interfejsu Gemini API (z Google AI Studio).
2. Konfiguracja: projekt i klucz interfejsu API
Tworzenie projektu Maven
Utwórz nowy projekt Maven w terminalu za pomocą tego polecenia:
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=gemini-interactions-demo \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.5 \
-DinteractiveMode=false
Przejdź do nowo utworzonego katalogu projektu:
cd gemini-interactions-demo
Otwórz plik pom.xml i skonfiguruj go:
- Zaktualizuj właściwości wersji Javy, aby kierować reklamy na Javę 21:
<properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> </properties> - Dodaj zależność pakietu SDK w bloku
:<dependency> <groupId>io.github.glaforge</groupId> <artifactId>gemini-interactions-api-sdk</artifactId> <version>0.10.1</version> </dependency>
Konfigurowanie klucza interfejsu API
Uzyskaj klucz interfejsu Gemini API w Google AI Studio.
Ustaw klucz jako zmienną środowiskową w terminalu:
macOS / Linux:
export GEMINI_API_KEY="your_actual_api_key"
Windows (wiersz polecenia):
set GEMINI_API_KEY="your_actual_api_key"
3. Hello World: poruszanie się po architekturze Step
Interfejs Interactions API wprowadził polimorficzną architekturę osi czasu opartą na krokach. Zamiast zwracać płaską listę danych wyjściowych, interfejs API zwraca sekwencję obiektów Step z określonym typem (np. ModelOutputStep, ThoughtStep, FunctionCallStep).
W tym kroku napiszesz prostą interakcję, aby dowiedzieć się, jak wyodrębnić z tej struktury końcowe dane wyjściowe modelu.
Utwórz: HelloInteractions.java
Utwórz plik src/main/java/com/example/HelloInteractions.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.InteractionParams.ModelInteractionParams;
public class HelloInteractions {
public static void main(String[] args) {
// 1. Initialize the client
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
// 2. Build the request
ModelInteractionParams request = ModelInteractionParams.builder()
.model("gemini-3.5-flash")
.input("Explain the difference between a library and a framework in one sentence.")
.build();
// 3. Send request
Interaction response = client.create(request);
// 4. Navigate the step-based architecture to get the output
response.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.map(step -> (Step.ModelOutputStep) step)
.findFirst()
.ifPresent(step -> System.out.println(step.content().get(0)));
}
}
Uruchom kod
Skompiluj i uruchom klasę:
mvn compile exec:java -Dexec.mainClass=com.example.HelloInteractions
4. Steerable Audio: Streaming Expressive TTS
Gemini 3.1 Flash wprowadza sterowaną zamianę tekstu na mowę (TTS). Za pomocą promptów możesz kontrolować tempo, ton i środowisko głosu, a w trakcie zdania używać tagów emocjonalnych (np. [excitedly] lub [whispers]).
W tym kroku wygenerujesz ekspresywny dźwięk i prześlesz go strumieniowo bezpośrednio do głośników.
Utwórz: StreamingDJ.java
Utwórz plik src/main/java/com/example/StreamingDJ.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.Config.SpeechConfig;
import io.github.glaforge.gemini.interactions.model.InteractionParams.ModelInteractionParams;
import javax.sound.sampled.*;
import java.util.Base64;
import java.util.stream.Stream;
public class StreamingDJ {
public static void main(String[] args) throws Exception {
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
// Prompt defining the voice profile and emotional tags
String prompt = """
# AUDIO PROFILE: Jaz R.
## THE SCENE: London Studio
### DIRECTOR'S NOTES
Accent: Jaz is a DJ from Brixton, London.
Style: Bouncy, energetic, high-speed delivery.
#### TRANSCRIPT
[excitedly] Yes, massive vibes in the studio!
[whispers] But keep it down, the boss is coming...
[shouting] Turn this up! Let's go!
""";
ModelInteractionParams request = ModelInteractionParams.builder()
.model("gemini-3.1-flash-tts-preview")
.input(prompt)
.responseModalities(Interaction.Modality.AUDIO)
.speechConfig(new SpeechConfig("Algenib", "en-GB"))
.stream(true) // Enable streaming
.build();
System.out.println("Streaming audio from Gemini...");
try (Stream<Events> eventStream = client.stream(request)) {
// Configure the Java Audio System for 24kHz Mono 16-bit PCM
AudioFormat format = new AudioFormat(24000, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
try (SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info)) {
line.open(format);
line.start();
// Process the stream and play audio chunks as they arrive
eventStream.forEach(event -> {
if (event instanceof Events.StepDelta cd && cd.delta() instanceof Events.AudioDelta audioDelta) {
byte[] audioData = Base64.getDecoder().decode(audioDelta.data());
line.write(audioData, 0, audioData.length);
}
});
line.drain();
}
}
}
}
Uruchom kod
mvn compile exec:java -Dexec.mainClass=com.example.StreamingDJ
Odsłuchaj wynik
Oto przykład dźwiękowy tego, co usłyszysz po uruchomieniu kodu (przy użyciu głosu Algenib z tagami emocjonalnymi):
5. Generowanie muzyki za pomocą Lyrii 3
Za pomocą modelu DeepMind Lyria 3 możesz generować muzykę i dżingle. Żądając dwóch trybów odpowiedzi (AUDIO i TEXT), możesz pobrać zarówno wygenerowany dźwięk (MP3), jak i tekst utworu.
Utwórz: MusicGenerator.java
Utwórz plik src/main/java/com/example/MusicGenerator.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.InteractionParams.ModelInteractionParams;
import io.github.glaforge.gemini.interactions.model.Content.AudioContent;
import java.nio.file.Files;
import java.nio.file.Paths;
public class MusicGenerator {
public static void main(String[] args) throws Exception {
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
ModelInteractionParams request = ModelInteractionParams.builder()
.model("models/lyria-3-clip-preview") // 30-second clip
.input("An uplifting rock song with acoustic guitars about coding in Java.")
.responseModalities(
Interaction.Modality.AUDIO,
Interaction.Modality.TEXT) // Request both MP3 and Lyrics
.build();
System.out.println("Generating music (this might take a moment)...");
Interaction response = client.create(request);
// 1. Print the lyrics (TEXT output)
System.out.println("\n--- Generated Lyrics ---");
response.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.flatMap(step -> ((Step.ModelOutputStep) step).content().stream())
.filter(content -> content instanceof Content.TextContent)
.forEach(content -> System.out.println(((Content.TextContent) content).text()));
// 2. Save the MP3 (AUDIO output)
response.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.flatMap(step -> ((Step.ModelOutputStep) step).content().stream())
.filter(content -> content instanceof AudioContent)
.map(content -> (AudioContent) content)
.findFirst()
.ifPresent(audio -> {
try {
Files.write(Paths.get("coding_song.mp3"), audio.data());
System.out.println("\nSuccess: Song saved to coding_song.mp3");
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
Uruchom kod
mvn compile exec:java -Dexec.mainClass=com.example.MusicGenerator
Słuchanie wygenerowanego utworu
Oto wygenerowany plik MP3 (coding_song.mp3) zawierający muzykę i tekst:
Posłuchaj wygenerowanego utworu muzycznego (coding_song.mp3)
6. Tworzenie wizualizacji za pomocą notatek wizualnych (Nano Banana Pro)
Gemini 3 Pro Image (znany też jako Nano Banana Pro) może generować obrazy. Po wybraniu trybu IMAGE możesz generować infografiki, diagramy lub notatki wizualne na podstawie wpisanego tekstu.
W tym kroku wygenerujesz podsumowanie artykułu o zarządzanych agentach w formie notatki wizualnej i zapiszesz je jako plik PNG.
Utwórz: ImageGenerator.java
Utwórz plik src/main/java/com/example/ImageGenerator.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.InteractionParams.ModelInteractionParams;
import io.github.glaforge.gemini.interactions.model.Content.ImageContent;
import java.nio.file.Files;
import java.nio.file.Paths;
public class ImageGenerator {
public static void main(String[] args) throws Exception {
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
String articleSummary = """
Managed Agents in the Gemini API allow developers to run autonomous agents
that reason, plan, use tools, and execute code inside isolated cloud sandboxes.
The Gemini API handles the infrastructure (containers, network, runtime).
It is powered by the Antigravity agent running on Gemini 3.5 Flash.
The Java Interactions SDK supports these capabilities, utilizing a Step-based
architecture to model the execution timeline.
""";
ModelInteractionParams request = ModelInteractionParams.builder()
.model("gemini-3-pro-image-preview")
.input(String.format("""
Create a hand-drawn and hand-written sketchnote
style summary infographic, with a pure white background,
about the following information:
%s
""", articleSummary))
.responseModalities(Interaction.Modality.IMAGE) // Request IMAGE modality
.build();
System.out.println("Generating sketchnote (this might take a moment)...");
Interaction response = client.create(request);
// Save the generated image
response.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.flatMap(step -> ((Step.ModelOutputStep) step).content().stream())
.filter(content -> content instanceof ImageContent)
.map(content -> (ImageContent) content)
.findFirst()
.ifPresent(image -> {
try {
Files.write(Paths.get("sketchnote.png"), image.data());
System.out.println("Success: Sketchnote saved to sketchnote.png");
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
Uruchom kod
mvn compile exec:java -Dexec.mainClass=com.example.ImageGenerator
Wygenerowany szkic
Oto wygenerowana notatka wizualna (sketchnote.png) utworzona przez model:

7. Sterowanie agentami: Deep Research we współpracy
Deep Research to zaawansowany agent, który może wykonywać wieloetapowe zadania badawcze. Zamiast jednak uruchamiać go od razu, możesz skorzystać z planowania opartego na współpracy, aby przejrzeć, zmodyfikować i skierować plan badań, zanim agent zacznie zbierać dane.
Wdrożysz wieloetapową rozmowę, która korzysta z tego samego stanu po stronie serwera (previousInteractionId), aby dopracować plan.
Utwórz: CollaborativeResearch.java
Utwórz plik src/main/java/com/example/CollaborativeResearch.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.InteractionParams.AgentInteractionParams;
import io.github.glaforge.gemini.interactions.model.Config.DeepResearchAgentConfig;
import io.github.glaforge.gemini.interactions.model.Config.ThinkingSummaries;
import io.github.glaforge.gemini.interactions.model.Config.Visualization;
public class CollaborativeResearch {
public static void main(String[] args) throws Exception {
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
String agentModel = "deep-research-preview-04-2026";
// --- Phase 1: Request a Plan ---
System.out.println("Phase 1: Requesting research plan...");
AgentInteractionParams planParams = AgentInteractionParams.builder()
.agent(agentModel)
.input("Research the latest generations of Google Cloud TPUs (TPU7x and the 8th generation TPU 8t and TPU 8i).")
.agentConfig(new DeepResearchAgentConfig(
"deep-research",
ThinkingSummaries.AUTO,
Visualization.AUTO,
true // TRUE enables collaborative planning
))
.background(true)
.store(true)
.build();
Interaction planInteraction = client.create(planParams);
planInteraction = waitForCompletion(client, planInteraction.id());
System.out.println("\n--- Proposed Plan ---");
printOutputText(planInteraction);
// --- Phase 2: Refine the Plan ---
System.out.println("\nPhase 2: Refining research plan...");
AgentInteractionParams refineParams = AgentInteractionParams.builder()
.agent(agentModel)
.input("Focus on comparing the architectural, performance, and scaling differences between the TPU7x generation and the two flavors of the eighth generation: TPU 8t (optimized for training at scale) and TPU 8i (optimized for low-latency reasoning and inference).")
.agentConfig(new DeepResearchAgentConfig(
"deep-research",
ThinkingSummaries.AUTO,
Visualization.AUTO,
true // Keep collaborative planning TRUE to iterate
))
.previousInteractionId(planInteraction.id()) // Resume session
.background(true)
.store(true)
.build();
Interaction refinedInteraction = client.create(refineParams);
refinedInteraction = waitForCompletion(client, refinedInteraction.id());
System.out.println("\n--- Refined Plan ---");
printOutputText(refinedInteraction);
// --- Phase 3: Approve and Execute ---
System.out.println("\nPhase 3: Approving plan and starting deep research (this will take a few minutes)...");
AgentInteractionParams executeParams = AgentInteractionParams.builder()
.agent(agentModel)
.input("Plan looks good, execute!")
.agentConfig(new DeepResearchAgentConfig(
"deep-research",
ThinkingSummaries.AUTO,
Visualization.AUTO,
false // FALSE approves the plan and executes the research
))
.previousInteractionId(refinedInteraction.id()) // Resume session
.background(true)
.store(true)
.build();
Interaction finalReport = client.create(executeParams);
finalReport = waitForCompletion(client, finalReport.id());
System.out.println("\n--- Final Research Report ---");
printOutputText(finalReport);
}
private static Interaction waitForCompletion(GeminiInteractionsClient client, String id) throws Exception {
Interaction interaction = client.get(id);
while (interaction.status() != Interaction.Status.COMPLETED && interaction.status() != Interaction.Status.FAILED) {
Thread.sleep(5000);
interaction = client.get(id);
}
if (interaction.status() == Interaction.Status.FAILED) {
throw new RuntimeException("Interaction failed. Status: " + interaction.status());
}
return interaction;
}
private static void printOutputText(Interaction interaction) {
interaction.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.flatMap(step -> ((Step.ModelOutputStep) step).content().stream())
.filter(content -> content instanceof Content.TextContent)
.forEach(content -> System.out.println(((Content.TextContent) content).text()));
}
}
Uruchom kod
mvn compile exec:java -Dexec.mainClass=com.example.CollaborativeResearch
Wygenerowane dane wyjściowe raportu
Agent Deep Research wygeneruje kompleksowy, uporządkowany raport. Pełny raport wygenerowany w ramach przykładowego uruchomienia znajdziesz tutaj:
Wyświetl wygenerowany raport Deep Research (tpu_history_report.md)
8. Agenty niestandardowe i sandbox w chmurze
W przypadku złożonych zadań programistycznych możesz udostępnić agentów niestandardowych. Określasz instrukcje systemowe, wyposażasz je w narzędzia (takie jak wykonywanie kodu lub powłoka bash) i konfigurujesz środowisko zdalne (np. reguły ruchu wychodzącego z sieci).
W tym kroku udostępnisz agenta, który ma bezpieczny dostęp do internetu do github.com, i polecisz mu sklonować repozytorium oraz przeanalizować pliki konfiguracyjne w piaskownicy w chmurze.
Utwórz: GitHubAnalyzer.java
Utwórz plik src/main/java/com/example/GitHubAnalyzer.java o tej zawartości:
package com.example;
import io.github.glaforge.gemini.interactions.GeminiInteractionsClient;
import io.github.glaforge.gemini.interactions.model.*;
import io.github.glaforge.gemini.interactions.model.InteractionParams.AgentInteractionParams;
import java.util.List;
public class GitHubAnalyzer {
public static void main(String[] args) throws Exception {
GeminiInteractionsClient client = GeminiInteractionsClient.builder()
.apiKey(System.getenv("GEMINI_API_KEY"))
.build();
String agentId = "github-analyzer-codelab";
// 1. Define the Custom Agent with Network Egress and Tools
Agent customAgent = Agent.builder()
.id(agentId)
.description("Clones and analyzes GitHub repos.")
.baseAgent("antigravity-preview-05-2026")
.baseEnvironment(new EnvironmentConfig(
new EnvironmentNetworkEgressAllowlist(List.of(
new AllowlistEntry("github.com") // Allow git clone over HTTPS
)),
List.of()
))
.systemInstruction("You are an architect. Clone the repo, inspect files, and write a summary.")
.tools(List.of(
new AgentTool.CodeExecution(), // Enables terminal bash execution in sandbox
new AgentTool.GoogleSearch()
))
.build();
// 2. Provision the Agent
System.out.println("Provisioning custom agent in the cloud...");
client.createAgent(customAgent);
try {
// 3. Start the Interaction
AgentInteractionParams params = AgentInteractionParams.builder()
.agent(agentId)
.input("Clone https://github.com/glaforge/gemini-interactions-api-sdk and explain its pom.xml structure.")
.environment("remote") // Crucial: Run in cloud sandbox
.build();
System.out.println("Starting clone and analysis (polling status)...");
Interaction interaction = client.create(params);
// 4. Poll for completion
while (interaction.status() != Interaction.Status.COMPLETED) {
System.out.println("Agent working... Status: " + interaction.status());
Thread.sleep(5000);
interaction = client.get(interaction.id());
}
// 5. Output the results
System.out.println("\n--- Architectural Analysis ---");
interaction.steps().stream()
.filter(step -> step instanceof Step.ModelOutputStep)
.flatMap(step -> ((Step.ModelOutputStep) step).content().stream())
.filter(content -> content instanceof Content.TextContent)
.forEach(content -> System.out.println(((Content.TextContent) content).text()));
} finally {
// 6. Clean up resources
client.deleteAgent(agentId);
System.out.println("\nCustom agent resource deleted from cloud.");
}
}
}
Uruchom kod
mvn compile exec:java -Dexec.mainClass=com.example.GitHubAnalyzer
Wygenerowane dane wyjściowe analizy
Pełny raport z analizy architektury wygenerowany przez agenta niestandardowego możesz wyświetlić po sklonowaniu repozytorium:
Wyświetlanie danych wyjściowych analizatora GitHub (github_analysis_report.md)
9. Gratulacje!
Ukończono ćwiczenie i dowiedziano się, jak tworzyć złożone, wielomodalne i agentowe przepływy pracy w języku Java za pomocą pakietu Gemini Interactions SDK.
Co udało Ci się osiągnąć:
- Korzystanie z architektury kroków: używanie nowej polimorficznej architektury kroków do wysyłania zapytań do modeli standardowych.
- Strumieniowana ekspresywna funkcja zamiany tekstu na mowę: wykorzystuje notatki reżysera i wbudowane tagi emocjonalne do strumieniowania dźwięku w czasie rzeczywistym.
- Wygenerowana muzyka: wygenerowane utwory MP3 i teksty za pomocą Lyrii 3.
- Wygenerowane notatki wizualne: tworzenie wizualnych podsumowań za pomocą Gemini 3 Pro Image (Nano Banana Pro).
- Deep Research: wykorzystywanie planowania opartego na współpracy do udoskonalania planów badań.
- Niestandardowi agenci z przydzielonymi zasobami: utworzone środowiska piaskownicy z niestandardową kontrolą ruchu wychodzącego z sieci, które umożliwiają bezpieczne wykonywanie kodu.
Więcej informacji:
- Kod źródłowy pakietu SDK i więcej przypadków testowych znajdziesz na GitHubie: glaforge/gemini-interactions-api-sdk
- Więcej informacji o wzorcach projektowania agentów znajdziesz na blogu Guillaume'a: glaforge.dev