SDK
7. fejezet

Tisztázó kérdezés

Ha egy tool nem tudja egyértelműen kinyerni a szükséges paramétereket a felhasználói kérdésből, interaktív Q&A körrel pontosíthatja az igényt – anélkül hogy a pipeline-t újraindítaná. Ez a fejezet az IClarifiableTool életciklusát, a ClarifyingQuestion összeállítását és a tipikus implementációs sémát mutatja be.

7.1   Mikor indokolt?

Az LLM-alapú paraméter-kinyerés legtöbbször elegendő: a tool az getInstruction()-ban leírt mezőket a felhasználó üzenetéből vonja ki. Tisztázó kérdezés akkor indokolt, ha egy kritikus paraméter hiánya miatt a keresés értelmetlen lenne (pl. szakrendelés-kereső életkorcsoport nélkül), és az opciók előre ismertek – tehát egy zárt vagy félig nyitott kérdéssel pótolhatók.

Ha az összes lehetséges paraméter-kombináció megfelelő eredményt ad, ne kérdezz vissza. A felesleges tisztázó kör ront a felhasználói élményen.

💡
MCP szervert használó tool-nál (AbstractMcpTool) a tisztázó kör életciklusát a platform kezeli: a tool a hívások előtt egyetlen metódushívással választást, szabad szöveges választ vagy hiányzó paramétereket kérhet a felhasználótól (→ 14.5).

7.2   IClarifiableTool interfész

Az IClarifiableTool<Request> kiterjeszti az IChatbotTool<Request> és az IOrchestratorActivatedTool interfészeket. Ebből következik:

Az interfész egyetlen saját metódusa:

ToolExecutionResult executeWithClarification(
    ClarificationSession session,
    FluxSink<TextGenerationResponse> progressEmitter,
    CallbackWithErrorHandler<OpenAIResponseVO, Exception> finalCallback
);

Az orchestrátor ezt hívja, amikor a felhasználó megválaszolt egy kérdést és az adott tool folytatásra vár. A session tartalmazza az eddigi részleges paramétereket és az épp megválaszolt kérdés azonosítóját.

ℹ️
A ClarificationSession a getSystems() accessoron keresztül a kérés eredeti system-szűrő listáját is elérhetővé teszi. Ezt az orchestrátor tölti fel az első tisztázó kérdés kiküldésekor, és a kérdés-válasz kör minden további fordulóján automatikusan megőrzi – egy executeWithClarification-t implementáló toolnak emiatt nem kell a system-listát saját maga elmentenie/visszatöltenie a részleges request JSON-jában, ha a folytatáshoz (pl. dokumentum-lekérdezéshez) szüksége van rá.

7.3   Az életciklus lépésről lépésre

1. executeWithResult()
      ↓ LLM kinyeri a paramétereket, de valami hiányzik
      → return ToolExecutionResult.needsClarification(question, partialJson, getName())

2. Platform: INTERACTIVE_QUESTION SSE → kliens megjelení a kérdést

3. Felhasználó válaszol → POST /api/chatbot/clarify  {chatId, answer}

4. executeWithClarification(session, ...)
      ↓ applyAnswer() alkalmazza a választ
      ↓ detectNextQuestion() megvizsgálja, kell-e még kérdés
      → ha igen: return needsClarification(...)   ← újabb kör
      → ha nem:  adatbázis / API hívás → return asGuaranteedContext(...)
ℹ️
A /api/chatbot/clarify végpont ugyanolyan SSE stream-et ad vissza, mint a /message. A válasz feldolgozása után a pipeline az utolsó DONE eseménnyel zárul – a kliensnek nem kell külön kezelnie.
ℹ️
A clarification session a platformon belül kezelt, korlátozott ideig él. Ha a felhasználó nem válaszol időben, a session lejár és a következő üzenet új kérésként indul.

7.4   ClarifyingQuestion összeállítása

A ClarifyingQuestion bean tartalmazza azt, amit a kliens megjelenít:

ClarifyingQuestion.builder()
    .questionId("adultOrChild")          // gépi azonosító – erre hivatkozik applyAnswer()
    .text("Felnőtt vagy gyermek szakrendelést keres?")
    .options(List.of("Felnőtt", "Gyermek", "Mindkettő érdekel"))
    .allowFreeText(false)                // false = csak az opciók egyike fogadható el
    .build();

7.5   Implementációs séma

Az alábbi minta egy tipikus interaktív, több kérdéskört kezelő tool felépítését mutatja. A következő három privát metódus alkotja a magtot – célszerű ezt a sémát követni:

detectNextQuestion() – mi hiányzik még?

Sorban ellenőrzi a kötelező paramétereket. Az első hiányos paraméterhez visszaad egy kérdést; ha minden megvan, null-lal tér vissza.

private ClarifyingQuestion detectNextQuestion(MyRequest request) {
    if (request.getAdultOrChild() == null
            || AdultOrChild.UNKNOWN.equals(request.getAdultOrChild())) {
        return ClarifyingQuestion.builder()
            .questionId("adultOrChild")
            .text("Felnőtt vagy gyermek szakrendelést keres?")
            .options(List.of("Felnőtt", "Gyermek", "Mindkettő érdekel"))
            .allowFreeText(false)
            .build();
    }
    if (request.getServicePlace() == null || request.getServicePlace().isBlank()) {
        return ClarifyingQuestion.builder()
            .questionId("servicePlace")
            .text("Melyik helyszínen keres szakrendelést?")
            .options(List.of("Debrecen", "Berettyóújfalu"))
            .allowFreeText(true)
            .build();
    }
    return null;  // minden megvan
}

applyAnswer() – a válasz alkalmazása

A questionId alapján meghatározza, melyik mezőt kell kitölteni, és az érkező szöveget a megfelelő típusra konvertálja.

private void applyAnswer(MyRequest request, String questionId, String userAnswer) {
    if ("adultOrChild".equals(questionId)) {
        String lc = userAnswer.toLowerCase();
        if      (lc.contains("feln"))                         request.setAdultOrChild(ADULT);
        else if (lc.contains("gyermek") || lc.contains("gyerek")) request.setAdultOrChild(CHILD);
        else                                                   request.setAdultOrChild(BOTH);
        // Ha egyik sem illeszkedik, marad UNKNOWN → következő körben újra kérdez
    } else if ("servicePlace".equals(questionId)) {
        request.setServicePlace(userAnswer.trim());
    }
}

executeWithRequest() – közös belső belépési pont

Mind az executeWithResult(), mind az executeWithClarification() ide hív be. Ez tartja össze a logikát: ha még hiányzik paraméter, visszakérdez; ha minden megvan, elvégzi a keresést.

private ToolExecutionResult executeWithRequest(MyRequest request) {
    try {
        ClarifyingQuestion nextQuestion = detectNextQuestion(request);
        if (nextQuestion != null) {
            String partialJson = objectMapper.writeValueAsString(request);
            return ToolExecutionResult.needsClarification(nextQuestion, partialJson, getName());
        }
        // Minden megvan – végrehajtás
        Set<UniqueTextDocument> docs = queryDatabase(request);
        return ToolExecutionResult.asGuaranteedContext(getName(), docs);
    } catch (Exception e) {
        logger.error("executeWithRequest hiba: {}", e.getMessage());
        return null;
    }
}

7.6   Teljes példa – IClarifiableTool vázlata

@Component
public class MyServiceTool implements IClarifiableTool<MyRequest> {

    private final ObjectMapper objectMapper;
    // ... egyéb függőségek

    @Override public String getName()     { return "my_service"; }
    @Override public String getDomain()   { return "MyDomain"; }
    @Override public int getPriority()    { return 0; }

    // ── Első kör: LLM kinyeri a paramétereket ──────────────────────────────
    @Override
    public ToolExecutionResult executeWithResult(List<String> systems, String userPrompt, String userLang,
            List<String> importantWords, FluxSink<TextGenerationResponse> emitter,
            CallbackWithErrorHandler<OpenAIResponseVO, Exception> cb) {
        try {
            Map<String, Object> resp = llmAssistant.generateToolRequest(userPrompt, SYSTEM_PROMPT, this);
            if (resp != null && resp.containsKey(getName())) {
                MyRequest request = objectMapper.convertValue(resp.get(getName()), MyRequest.class);
                return executeWithRequest(request);
            }
        } catch (Exception e) { /* log */ }
        return null;
    }

    // ── Folytatás: felhasználó megválaszolta a kérdést ─────────────────────
    @Override
    public ToolExecutionResult executeWithClarification(ClarificationSession session,
            FluxSink<TextGenerationResponse> emitter,
            CallbackWithErrorHandler<OpenAIResponseVO, Exception> cb) {
        try {
            MyRequest request = objectMapper.readValue(
                session.getPartialRequestJson(), MyRequest.class);
            applyAnswer(request, session.getPendingQuestionId(), session.getPendingAnswer());
            return executeWithRequest(request);
        } catch (Exception e) {
            logger.error("Clarification folytatás sikertelen: {}", e.getMessage());
            return null;
        }
    }

    // ── Privát segédmetódusok ──────────────────────────────────────────────
    private ToolExecutionResult executeWithRequest(MyRequest request) { ... }
    private ClarifyingQuestion detectNextQuestion(MyRequest request)  { ... }
    private void applyAnswer(MyRequest request, String questionId, String answer) { ... }

    // ── IChatbotTool – kötelező metódusok (getRequestAsJsonNode() nincs
    // felülírva, a default interfész-implementáció elegendő) ──────────────
    @Override public String getInstruction()          { return "..."; }
    @Override public Class<MyRequest> getRequestClass() { return MyRequest.class; }
    @Override public String getExecutableInstruction() { return "my_service: true ha ..."; }
}
💡
A fenti minta egy valós referencia implementáción alapul (priority=0), amely két egymást követő kérdéskört kezel: felnőtt/gyermek szűrő, majd helyszín.

7.7   Több tool visszakérdezése egy körben

Ha egy prioritáscsoportban több IClarifiableTool is NEEDS_CLARIFICATION-nel tér vissza, a platform automatikusan sorba állítja a kérdéseket: az első azonnal kiküldésre kerül, a többi az előző megválaszolása után következik. A plugin fejlesztőnek ezzel nincs teendője.

⚠️
A ClarificationSession objektum az executeWithClarification() hívásban read-only – ne módosítsd közvetlenül, csak az általa nyújtott adatokat olvasd.

7.8   Multi-hop és clarification egyidejűleg

Ha az orchestrátor multi-hop feldolgozás közben küld ki NEEDS_CLARIFICATION-t, a platform védi a párhuzamos hozzáférést: a felhasználó válasza csak akkor kerül feldolgozásra, amikor az orchestrátor már befejezte a folyamatban lévő munkát. Szerverleállás esetén a védelem automatikusan feloldódik – nincs szükség manuális beavatkozásra.

Plugin fejlesztőnek ezzel nincs teendője – az AbstractOrchestratorTool kezeli.