SDK
5. fejezet

Orchestrátor tool fejlesztés

Az orchestrátor a domain-specifikus vezérlési logika otthona – minden kérés ide csatornázódik be először. Ez a fejezet az implementáció teljes képét mutatja, beleértve a NER ingestion tool kapcsolatát, amely a domain grafikus tudásának egyetlen forrása.

5.1   Mi az orchestrátor és mikor kell?

Az orchestrátor akkor indokolt, ha a domain kérései egynél összetettebb feldolgozást igényelnek: több tool koordinálása, sub-query bontás, domain-specifikus döntési logika vagy deep fallback keresés szükséges. Minden kérés – domain egyezés esetén – először az orchestrátoron megy keresztül, amelynek lehetősége van tool-okat aktiválni, a kérdést al-kérdésekre bontani, vagy visszalépni a RAG pipeline-ba (null visszatérés).

Ha a domainhez nincs regisztrált (Spring-managed vagy plugin) orchestrátor, a platform két módon reagálhat – lásd lentebb. Az orchestrátor tehát a plugin fejlesztő szempontjából opcionális – de ha van, teljes kontrollt kap.

Több orchestrátor egy domainhez – system alapú kiválasztás

Egy domainhez több orchestrátor is regisztrálható – jellemzően úgy, hogy mindegyik a domain más-más system-jéhez tartozik (getEligibleSystem()). Kérésenként azonban mindig pontosan egy orchestrátor aktiválódik: a platform először a domain egyezés alapján szűr, majd a jogosultságot nézi – egy orchestrátor akkor jogosult, ha getEligibleSystem() null-t ad vissza (tehát bármely kérésre illik), vagy a kérésben egyáltalán nincs system megadva, vagy a kérés system listája tartalmazza az orchestrátor eligible system-jét.

A jogosult jelöltek közül a kiválasztás a következő elv szerint történik: ha a kérésben nincs system megadva, a null eligible system-ű (domain-szintű alapértelmezett) orchestrátor élvez elsőbbséget a system-specifikusakkal szemben. Ha a kérésben van system, és emiatt egynél több jelölt is jogosult marad, a platform egyet választ közülük, de ez a választás nem determinisztikus prioritási sorrend alapján történik.

⚠️
Ha ugyanazon domainhez több system-specifikus orchestrátort regisztrálsz, ügyelj rá, hogy egy adott kérés system listája lehetőleg csak egyetlen orchestrátorhoz illeszkedjen – két egyszerre jogosult, system-specifikus orchestrátor közötti választás nem garantáltan ugyanazt az eredményt adja minden kérésre.

Ha nincs regisztrált orchestrátor a domainhez – a platform default orchestrátora

Ha a kért domain+system, illetve domain-only szűrésre sem talál a platform regisztrált (Spring-managed vagy plugin) orchestrátort, a döntés a domain admin-felületen beállított defaultOrchestrator kapcsolóján múlik:

A defaultOrchestrator kapcsoló kizárólag a domain admin konfigurációjának része – a plugin fejlesztőnek nincs ráhatása és nem is kell vele foglalkoznia. Ha egy plugin később valódi, domain-specifikus orchestrátort regisztrál, az a következő kérésnél automatikusan átveszi a vezérlést a default orchestrátortól; ha a plugin eltávolításra kerül, a platform automatikusan visszaesik a default orchestrátorra (vagy a defaultOrchestrator=false esetben az orchestrátor nélküli módra) – ehhez sincs szükség semmilyen plugin-oldali teendőre.

5.2   Az osztályhierarchia

AbstractOrchestrator          ← a domain tudás és hook metódusok otthona
  └── AbstractOrchestratorTool ← a platform pipeline integráció (final metódusok)
        └── MyOrchestratorTool ← a plugin fejlesztő implementációja

A plugin fejlesztő mindig az AbstractOrchestratorTool-ból örököl. Az AbstractOrchestrator tartalmazza a domain-tudás hook-jait (gráf szűrők, deep fallback), az AbstractOrchestratorTool pedig a pipeline integrációt (clarification session kezelés, sub-query bontás, prioritás-csoportos tool futtatás). Az executeWithResult() és az executeOrchestratorActivatedTools() metódusok final-ok – nem írhatók felül.

5.3   Kötelező elemek

Annotációk

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // kötelező – per-request mutable állapot
public class MyOrchestratorTool extends AbstractOrchestratorTool {
⚠️
A @Scope("prototype") nem elhagyható. Az orchestrátor per-request mutable állapotot tárol (progressEmitter, finalCallback, chatId, userLang, classification, retrievalOptions). Singleton esetén párhuzamos kérések felülírnák egymás állapotát.

Konstruktor

A plugin-aktiválási mechanizmus miatt minden plugin tool és orchestrátor osztálynak publikus, paraméter nélküli konstruktorral kell rendelkeznie – ez alól az AbstractOrchestratorTool leszármazottak sem kivételek. A discovery Java ServiceLoader-en keresztül történik (ami maga is no-arg konstruktort követel meg minden felfedezett osztálytól), és orchestrátorok esetén a PluginLoader is közvetlenül, getDeclaredConstructor().newInstance()-szel példányosít – paraméter nélkül. Az AbstractOrchestrator ősosztály függőségei (orchestratorAssistant, objectMapper, toolRegistry stb.) @Autowired mezőként vannak felvéve már az alaposztályban; ezeket a példányosítás után a platform (autowireBean()) automatikusan feltölti – nincs szükség rájuk konstruktor-paraméterként.

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {

    public MyOrchestratorTool() {
        super();
    }

    @Override
    public String getDomain() {
        return "MyDomain";
    }
}

Mivel a konstruktor paraméter nélküli, a domain NER ingestion tool-ját nem lehet konstruktoron keresztül átadni. Helyette a NER tool-t @Autowired mezőként kell felvenni, és egy @PostConstruct metódusban regisztrálni az AbstractOrchestrator védett setNerIngestionTool(...) setterén keresztül – ez már azután fut le, hogy a platform az autowireBean()-nel feltöltötte a mezőket:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {

    @Autowired
    private MyNERIngestionTool myNerIngestionTool;

    public MyOrchestratorTool() {
        super();
    }

    @PostConstruct
    public void init() {
        setNerIngestionTool(myNerIngestionTool);
    }

    @Override
    public String getDomain() {
        return "MyDomain";
    }
}

A NER ingestion tool bekötéséről (mit ad át, mire használja az orchestrátor) részletesen az 5.7 alfejezet szól.

getDomain()

@Override
public String getDomain() {
    return "MyDomain";  // egyezik a platform admin által konfigurált domain névvel
}

5.4   Az orchestrate() metódus

Ez az egyetlen kötelezően implementálandó abstract metódus. Minden kéréshez (vagy sub-queryhez, ha multi-hop) meghívódik.

protected abstract ToolExecutionResult orchestrate(
    List<String> systems,   // aktív system szűrők
    TaskInfo taskInfo, // az al-kérdés szövege, típusa, fontos szavak
    RetrievalOptions opts   // az osztályozásból levezetett retrieval profil
);

Visszatérési értékek:

A két megközelítés

1. Közvetlen tool hívás – az orchestrátor maga dönt, melyik tool-t hívja és mikor. Tipikus eset: PromptType alapján elágazás.

@Override
protected ToolExecutionResult orchestrate(List<String> systems,
        TaskInfo taskInfo, RetrievalOptions opts) {
    PromptType type = taskInfo.getPromptType();
    if (PromptType.REASONING.equals(type) || PromptType.COMPARATIVE.equals(type)) {
        // Entitás-elemző tool közvetlen hívása
        ToolExecutionResult res = characterTool.executeWithResult(
            systems, taskInfo.getTaskPrompt(), userLang, taskInfo.getImportantWords(),
            progressEmitter, finalCallback);
        if (res != null) return res;
    }
    // Ha nem volt aktiválható tool, delegálunk vagy null-t adunk vissza
    return null;
}

2. Delegálás az automatikus rendszernek – az orchestrátor átadja a döntést az executeOrchestratorActivatedTools()-nak, amely az összes regisztrált IOrchestratorActivatedTool-t aktiválja prioritás szerint.

@SuppressWarnings("rawtypes")
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
        TaskInfo taskInfo, RetrievalOptions opts) {
    // Ha egyes tool-okat ki akarunk zárni az automatizmusból:
    setExcludedTools(List.of(contextSearchTool.getName()));
    return executeOrchestratorActivatedTools(
        (Map) CurrentUserUtil.getCurrentUser(), systems, taskInfo, opts);
}

A két megközelítés kombinálható is: egyes PromptType-oknál közvetlen hívás, másokra delegálás.

5.5   executeOrchestratorActivatedTools()

Az automatikus tool futtatás engine-je. Az összes regisztrált IOrchestratorActivatedTool-t aktiválja a domain-ben:

Ha az orchestrátor kizár bizonyos tool-okat az automatizmusból (mert maga kezelte):

setExcludedTools(List.of("summary_tool", "character_tool"));

A userData paraméter – saját jogosultsági logika

Az executeOrchestratorActivatedTools() első paramétere (userData) minden egyes aktiválni kívánt tool isEligible(Map userData) hívásába változatlanul továbbadódik – ez dönti el, hogy egy tool egyáltalán szóba jöhet-e az adott felhasználónak, még az LLM routing előtt. A platform a saját autentikációs rendszerében (Keycloak) azonosítja a hívó felhasználót; ezt a CurrentUserUtil-on keresztül lehet lekérdezni.

A userData paraméter típusa nyers Map, és a platform nem írja elő, minek kell benne lennie. Ki, mihez jogosult – ez a döntés nem a platform, hanem a plugin fejlesztőjének hatásköre. A minta példákban látható (Map) CurrentUserUtil.getCurrentUser() csak egy kényelmi alapeset, amikor a plugin-nak nincs saját jogosultsági rétege, és egyszerűen a platform user objektumát adja tovább. Ha a domain tool-jai nem mindenki számára futtathatók, a plugin felépítheti a saját autorizációs logikáját is:

@SuppressWarnings("rawtypes")
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
        TaskInfo taskInfo, RetrievalOptions opts) {
    Long platformUserId = CurrentUserUtil.getCurrentUserId();
    // A saját autorizációs rendszer id alapján ad vissza jogosultsági attribútumokat –
    // ennek tartalma és formája kizárólag a plugin fejlesztőjén múlik.
    Map userData = myAuthorizationService.resolvePermissions(platformUserId);
    return executeOrchestratorActivatedTools(userData, systems, taskInfo, opts);
}

Az így összeállított userData map jelenik meg ezután minden tool isEligible(Map userData) implementációjában – a platform user objektumától függetlenül, tetszőleges saját kulcsokkal és attribútumokkal.

5.6   Hook metódusok

usePerSubQueryAnswering()

Multi-hop esetén (ha az LLM a kérdést több al-kérdésre bontja) dönti el, hogy az al-kérdések egyenként kapnak-e saját LLM kört és válasz szöveget, vagy az összes kontextus egyetlen LLM hívásba kerül.

// Alapértelmezett: false – egyetlen összevont LLM kör
// true: al-kérdésenként külön LLM kör (SEGMENT_BREAK SSE-vel elválasztva)
@Override
protected boolean usePerSubQueryAnswering() {
    return true;
}
ℹ️
true esetén a pipeline méri a kontextus-előállítási időt, és ha az gyors volt, automatikusan synthesis módra vált: az al-válaszokat összegyűjti, majd egyetlen szintézis LLM hívással generál végső választ. Ha lassú volt, a szegmensek egymás után streamelnek SEGMENT_BREAK-kel. Ez a platform saját, automatikus szegmentálása – egy tool ANSWERED módban saját maga is küldhet SEGMENT_BREAK-et, ettől függetlenül (→ 8.4).

getDeepFallbackTool(PromptType)

Ha a kontextus gyenge vagy üres, a pipeline ezt a tool-t aktiválja mélyebb analízisre. Tipikusan egy AbstractContextSearchTool leszármazott: entitás-alapú gráf keresés, gráf-találat hiányában (nincs azonosítható entitás, vagy van entitás, de nincs gráf-egyezés) LLM-mel kiválasztott kifejezésre BM25 kulcsszavas keresés, és csak ha egyik sem talál, szűretlen teljes domain-átvizsgálás – majd Spark-alapú batch értékelés dönti el, elegendő-e a talált kontextus.

@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
    return contextSearchTool;  // minden PromptType-ra ugyanaz a fallback
    // Vagy típusra szabva:
    // return PromptType.ENTITY_LOOKUP.equals(type) ? entitySearchTool : contextSearchTool;
}

Ha null-t adsz vissza (alapértelmezett), a deep fallback nem aktiválódik.

A deep fallback akkor indul, ha a kontextus-kapuőr pontszáma a spring.retriever.context-sufficiency-threshold küszöb (alapértelmezés: 60) alatt marad, és a kérdés a domainhez kapcsolódik. Ajánlott a kontextuskereső tool helyett a platform mélykereső coordinatorának leszármazottját visszaadni, amely a kereső tool-t célzott keresésekre használja - bekötése és paraméterezése: 6.10.

@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
    return deepSearchCoordinatorTool;  // AbstractDeepSearchCoordinatorTool leszármazott
}
ℹ️
Nagy corpuszú domainen az AbstractContextSearchTool leszármazottja felülírhatja a getMaxBruteForcePageCount() hook-ot (alapértelmezés: null, nincs korlát). Beállított érték esetén a szűretlen, teljes domain-átvizsgálás egyáltalán nem fut le, ha sem a gráf-, sem a BM25-ág nem talál oldalt; emellett a gráf- vagy BM25-ágról érkező találathalmaz mérete is ehhez az értékhez van mérve, mielőtt a Spark-alapú, batch-enkénti kontextus-elégségességi LLM-hívások elindulnának – efölött a tool a mély-kereső LLM-hívásokat kihagyva null-lal tér vissza.

getClassificationPromptSupplement()

Domain-specifikus kiegészítő szöveg a sub-query bontási LLM classification promptjához. Alapértelmezetten null – ilyenkor a prompt változatlan marad. Ha nem null/üres a visszatérési érték, a szöveg a bontási alapszabályok után kerül a promptba. Jó eszköz arra, hogy egy domain-specifikus mondatszerkezetet kizárj a felesleges al-kérdésekre bontásból (pl. egy állítás és annak valószínűsége/súlyossága egyetlen összefüggő kérdésnek számítson).

@Override
protected String getClassificationPromptSupplement() {
    return """
            Egy adott állapot fennállására és annak valószínűségére, súlyosságára vagy
            alátámasztó/cáfoló körülményeire vonatkozó kérdés mindig egyetlen összefüggő
            kérdésnek számít – ezeket ne bontsd külön al-kérdésekre.
            """;
}

getDefaultAnswer()

Akkor releváns, ha egy domainhez több, system-enként külön regisztrált orchestrátor tartozik (a kérés system megjelölésével aktiválja a megfelelőt). Ha egy ilyen, konkrét system-hez kötött orchestrátor aktiválódott, de a kérésre egyetlen tool sem reagált, az alapértelmezett viselkedés (null visszaadása) a RAG pipeline-nak adja át a vezérlést az opts profil alapján. Ha a domain ezen system-jéhez nem tartozik statikus RAG adatbázis (pl. az orchestrátor kizárólag tool-okon keresztül dolgozik), ez a hook felülírható, hogy ehelyett egy kész választ adjon vissza, amely jelzi, hogy a kérés nem tartozik az aktivált agent hatáskörébe.

@Override
protected ToolExecutionResult getDefaultAnswer() {
    return ToolExecutionResult.asAnswer(getName(), TextAnswer.asSet("""
            A kérdés alapján nem volt azonosítható olyan feladat, ami ennek az
            agentnek a hatásköréhez tartozna.
            """));
}
⚠️
Ha getDefaultAnswer()-t felülírod, mert a system-hez nem tartozik statikus RAG adat, az orchestrate()-ben zárd ki a RAG retrieval tool-t is setExcludedTools(List.of("rag_retrieval_tool"))-val – különben az mindig fallback-ként aktiválódna, és irreleváns kontextust adna, mielőtt a getDefaultAnswer() egyáltalán lefutna.

usePageLevelIngestion(String system)

Domain+system alapú döntés arról, hogy az adott system nagy korpusza miatt a mondat-szintű helyett a page-level ingestion/retrieval mód indokolt-e: true esetén nincs mondat-szintű DocumentEmbedding1024 collection az adott system-hez – kizárólag az oldalak pontos szövegének egyetlen vektora és BM25-bejegyzése készül el, és a retrieval is ezen fut. Alapértelmezetten false minden system-re – visszafelé kompatibilis, a meglévő domainek viselkedése nem változik.

@Override
public boolean usePageLevelIngestion(String system) {
    return "NAGY_CORPUS_SYSTEM".equals(system);
}

A hook mind az ingestion pipeline-t (az ingestion time hívó a domainhez tartozó, system-hez illő orchestrátort keresi meg és kérdezi le), mind a chat-time retrieval profilt (RetrievalOptions.pageLevelMode()) egyetlen tudásforrásból vezérli. Ugyanazon domainen belül system-enként eltérő lehet – egy domain néhány, már felöltött, kis korpuszú system-je maradhat mondat-szintű, míg egy új, nagy korpuszú system page-level módban ingestálódik. Részletek: 9.5 alfejezet.

⚠️
Egy ingestion batch akkor megy page-level útra, ha a benne szereplő összes megkülönböztethető system page-level módban üzemel az adott domainen. Kevert batch (részben már felöltött, mondat-szintű system-ek, részben page-level system-ek egy batchen belül) esetén a biztonságosabb, meglévő mondat-szintű feldolgozás marad érvényben az egész batchre.

getContextExpansionMode(PromptType)

Kérdéstípusonként megadja, mit kapjon az LLM a keresési találatokból: csak a releváns mondatokat (SENTENCE_ONLY), a mondatokat és az oldalak összefoglalóját (PAGE_SUMMARY), a találati oldalak teljes szövegét (FULL_PAGE), vagy a kontextus méretétől függő választást (AUTO). Alapértelmezetten a FACTOID, ENTITY_LOOKUP és DEFINITION típus PAGE_SUMMARY, a PROCEDURAL, COMPARATIVE és REASONING típus FULL_PAGE, a CONTEXTUAL típus AUTO. Akkor érdemes felülírni, ha a domain korpuszán más mód ad megbízhatóbb kontextust, például ha az oldal-összefoglalók nem elég pontosak, és minden kérdéshez a teljes oldal kell.

@Override
protected ContextExpansionMode getContextExpansionMode(PromptType type) {
    return ContextExpansionMode.FULL_PAGE;
}

5.7   A NER ingestion tool bekötése – az egyetlen tudásforrás elve

Az orchestrátorba opcionálisan bekötheted a domain NER ingestion tool-ját (@Autowired mező + @PostConstruct init, lásd 5.3). Ez egy kulcsfontosságú tervezési döntés: a domain-specifikus tudás (entitástípusok, relációtípusok, PromptType→gráf mapping) egyetlen helyen él, és mind az ingestion pipeline, mind az orchestrátor onnan olvassa.

Ha be van kötve az ingestion tool, az orchestrátor automatikusan delegál:

Orchestrátor hook Honnan jön az érték
getGraphRelationTypesForPromptType(type) nerIngestionTool.getRelationTypesForPromptType(type)
isGraphBoostEnabled(type) nerIngestionTool.isGraphBoostEnabled(type)

Ezeket nem kell az orchestrátorban felülírni – elég az ingestion tool-ban definiálni. Ha nincs ingestion tool bekötve, a gráf bejárás nem szűrt (minden él követhető) és a graph boost ki van kapcsolva.

5.8   AbstractNERIngestionTool leszármaztatása

A NER ingestion tool a domain grafikus tudásának gazdája. Az alábbi metódusokat kell implementálni:

getEligibleSystem()

Megadja, hogy ez a tool melyik system értékű dokumentumokat dolgozza fel. Az ingestion pipeline a domain összes NER tool-ját system-szűrés nélkül futtatja le, így null esetén az ingestion közben az összes dokumentum feldolgozásra kerül a domainben. Ha a domain több forrásból tölt be tartalmat (pl. orvosi PDF-ek és weboldal-scrape), domainenként több NER tool is regisztrálható – mindegyik a saját system-jét kezeli.

@Override
public String getEligibleSystem() {
    return "DOCUMENTS";  // null = minden dokumentum a domainben (ingestion közben)
}
⚠️
A null visszatérés csak az ingestion pipeline-ban jelent univerzális elérhetőséget. Ha valamelyik retrieval tool a system-szűrt ToolRegistry.getIngestionTools(domain, systems) lekérdezéssel ellenőrzi a gráf-eligibility-t (pl. a graph enrichment bekapcsolásához), egy null deklarációjú NER tool nem fog megjelenni, ha a kérésben explicit system szűrő van – ilyenkor az IChatbotTool.ALL-t kell visszaadni null helyett.
ℹ️
Több forrásból táplált domainre jó példa, ha az egyik NER tool (system="DOCUMENTS") a feltöltött dokumentumokból épít fogalmakat és relációkat, míg egy másik NER tool (system="WEB") egy weboldal-scrape alapján szervezeti, infrastrukturális entitásokat keres. Az orchestrátorba csak az elsődleges NER tool kerül bekötésre (@Autowired mező + @PostConstruct init, lásd 5.3) – az határozza meg a gráfbejárás szűrőit és a boost logikát.

defineEntityTypes()

A domain entitástípusait és azok leírását definiálja. Az EntityTypeDescriptor két paramétert vár: a típus nevét (NAGY_BETŰS) és egy természetes nyelvű leírást az LLM számára. Ebből automatikusan épül fel:

@Override
protected List<EntityTypeDescriptor> defineEntityTypes() {
    return List.of(
        new EntityTypeDescriptor("DISEASE",    "betegségek, kórképek, tünetek, állapotok"),
        new EntityTypeDescriptor("DRUG",       "gyógyszerek, hatóanyagok, készítmények"),
        new EntityTypeDescriptor("PROCEDURE",  "beavatkozások, vizsgálatok, kezelési eljárások"),
        new EntityTypeDescriptor("ANATOMY",    "anatómiai struktúrák, szervek, testrészek"),
        new EntityTypeDescriptor("OTHER",      "egyéb fontos fogalom")
    );
}

defineRelationTypes()

A domain relációtípusait definiálja. Ugyanúgy automatikusan épül be a NER promptba.

@Override
protected List<RelationTypeDescriptor> defineRelationTypes() {
    return List.of(
        new RelationTypeDescriptor("HAS_SYMPTOM",        "betegségnek adott tünete van"),
        new RelationTypeDescriptor("TREATED_BY",         "betegség kezelési módja"),
        new RelationTypeDescriptor("CONTRAINDICATED_WITH","ellenjavallat két entitás között"),
        new RelationTypeDescriptor("CAUSES",             "okozati összefüggés"),
        new RelationTypeDescriptor("RELATED_TO",         "általános tematikus kapcsolat")
    );
}

getAdditionalRules() – opcionális

Extra szabályok a NER prompthoz (pl. ragozási elvek, kizárási feltételek, szimbolikus vs. köznapi tárgy megkülönböztetése).

@Override
protected String getAdditionalRules() {
    return """
             - A label mező MINDIG alany esetben álljon – ragozott alakokat soha ne adj label-ként
             - DRUG típust csak konkrét hatóanyagra vagy készítményre adj, nem terápiás csoportra
            """;
}

getRelationTypesForPromptType(PromptType) – az orchestrátor is ezt kapja

PromptType-onként szűri, hogy a gráf bejáráskor milyen éltípusok követhetők. Ez az orchestrátor getGraphRelationTypesForPromptType()-ja által meghívott metódus – egyetlen helyen definiált, mindkettő ebből olvas.

@Override
public Set<String> getRelationTypesForPromptType(PromptType type) {
    return switch (type) {
        case PROCEDURAL -> Set.of("TREATED_BY", "HAS_SYMPTOM", "PART_OF");
        case REASONING  -> Set.of("CAUSES", "HAS_SYMPTOM", "TREATED_BY");
        case COMPARATIVE-> Set.of("CONTRAINDICATED_WITH", "RELATED_TO");
        case FACTOID    -> Set.of("PART_OF", "CAUSES");
        default         -> null;  // null = minden él követhető
    };
}

isGraphBoostEnabled(PromptType)

Megadja, hogy az adott PromptType-nál a tudásgráf által azonosított oldalak kapnak-e score-boostot a rerank előtt.

@Override
public boolean isGraphBoostEnabled(PromptType type) {
    return List.of(PromptType.DEFINITION, PromptType.ENTITY_LOOKUP, PromptType.FACTOID)
               .contains(type);
}

Hangolható normalizálási paraméterek – opcionális

Az alaposztály két küszöbértéket alkalmaz az entitás- és alias-zaj kiszűrésére az ingestion pipeline végén. Mindkettő felülírható, ha a korpusz mérete vagy az LLM megbízhatósága eltér az alapértelmezettől.

getAliasMinPageCount() – egy alias csak akkor kerül a gráfba, ha legalább ennyi különböző oldalon visszaigazolódik. Alapértelmezett: 2. Kis korpusznál (egyetlen könyv, néhány dokumentum) 1-re csökkenthető; zajos LLM esetén 3-ra vagy magasabbra emelhető.

@Override
protected int getAliasMinPageCount() {
    return 1;  // alapértelmezett: 2
}

getLowFrequencyEntityThreshold() – az ennél kevesebb oldalon megjelenő entitások „ritka"-nak minősülnek, és a pipeline token-laza illesztéssel próbál etablált anyaentitást keresni számukra (ragozott vagy elírt alakok megmentése). Alapértelmezett: 2.

@Override
protected int getLowFrequencyEntityThreshold() {
    return 1;  // alapértelmezett: 2
}
ℹ️
A normalizálás pipeline sorrendben fut: (1) keresztoldal kanonizálás, (2) kölcsönös alias-összevonás, (3) low-frequency rescue. A getAdditionalRules()-ban ezért nem szükséges explicit névegyesítési utasításokat megadni – a pipeline automatikusan kezeli a névformák összevonását. Az additional rules inkább a típus-elhatárolásra és a kizárási feltételekre való.

5.9   Teljes példa – minimális orchestrátor

Az alábbi példa egy tiszta delegálásra épülő orchestrátor mintáját mutatja: minden kérést az automatikus tool rendszernek delegál, engedélyezi a multi-hop per-segment válaszadást, és bekötött NER ingestion tool-lal rendelkezik.

// ── NER Ingestion Tool ─────────────────────────────────────────────────────

@Component
public class MyNERIngestionTool extends AbstractNERIngestionTool {

    public MyNERIngestionTool() {
        super();
        // A SparkSessionStorage és a KnowledgeGraphService @Autowired(required=false)
        // settereken keresztül töltődik fel az alaposztályban, a példányosítás után.
    }

    @Override public String getDomain()         { return "MyDomain"; }
    @Override public String getEligibleSystem() { return null; }  // teljes domain

    @Override
    protected List<EntityTypeDescriptor> defineEntityTypes() {
        return List.of(
            new EntityTypeDescriptor("PERSON",   "személyek, szakértők"),
            new EntityTypeDescriptor("CONCEPT",  "fogalmak, elvek, módszerek"),
            new EntityTypeDescriptor("OTHER",    "egyéb fontos fogalom")
        );
    }

    @Override
    protected List<RelationTypeDescriptor> defineRelationTypes() {
        return List.of(
            new RelationTypeDescriptor("RELATED_TO", "általános kapcsolat"),
            new RelationTypeDescriptor("CAUSES",     "okozati összefüggés")
        );
    }

    @Override
    public Set<String> getRelationTypesForPromptType(PromptType type) {
        return null;  // minden él követhető minden típusnál
    }

    @Override
    public boolean isGraphBoostEnabled(PromptType type) {
        return PromptType.ENTITY_LOOKUP.equals(type);
    }
}


// ── Orchestrátor ───────────────────────────────────────────────────────────

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {

    @Autowired
    private MyContextSearchTool contextSearchTool;

    @Autowired
    private MyNERIngestionTool myNerIngestionTool;

    public MyOrchestratorTool() {
        super();
    }

    @PostConstruct
    public void init() {
        // A gráf tudás (reláció szűrők, boost) a nerIngestionTool-ból automatikusan jön
        setNerIngestionTool(myNerIngestionTool);
    }

    @Override
    public String getDomain() { return "MyDomain"; }

    @SuppressWarnings("rawtypes")
    @Override
    protected ToolExecutionResult orchestrate(List<String> systems,
            TaskInfo taskInfo, RetrievalOptions opts) {
        // Az összes IOrchestratorActivatedTool-t aktiválja prioritás szerint
        // (contextSearchTool kizárva, mert deep fallback tool, nem normál aktiválandó)
        setExcludedTools(List.of(contextSearchTool.getName()));
        return executeOrchestratorActivatedTools(
            (Map) CurrentUserUtil.getCurrentUser(), systems, taskInfo, opts);
    }

    @Override
    protected boolean usePerSubQueryAnswering() {
        return true;  // multi-hop: al-kérdésenként külön LLM válasz
    }

    @Override
    protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
        return contextSearchTool;  // gyenge kontextus esetén mélyebb keresés
    }
}
💡
Referencia implementáció a kódbázisban: DemoSystemOrchestratorTool (közvetlen tool hívás PromptType alapján + delegálás kombinálva), a hozzá tartozó DemoNERIngestionTool pedig az entitás- és relációtípusok definiálásának módját mutatja. Tiszta delegálásra és per-segment multi-hopra épülő orchestrátor mintáját a fenti kódrészlet adja.

5.10   getIdentityDescription() – a chatbot-identitás válasz tartalma

A platform egy beépített, minden domain-en aktiválódó tool-lal (IdentityTool) válaszol az olyan kérdésekre, amelyek nem a tudásbázis tartalmára, hanem magára a chatbotra vonatkoznak – „ki vagy te", „mit tudsz", „miben tudsz segíteni", „milyen rendszerekhez van hozzáférésed". Ezekre a kérdésekre nincs válasz a tudásbázis dokumentumaiban, ezért az IdentityTool nem RAG kontextust ad az LLM-nek, hanem a regisztrált orchestrátorok és tool-ok getIdentityDescription() visszatérési értékeiből épít saját, dedikált system prompt-ot, és maga hívja meg az LLM-et.

A metódus az IChatbotTool-on van deklarálva (default null), tehát bármely orchestrátor és bármely sima tool felülírhatja. A választás, hogy hol érdemes megírni a leírást, a plugin telepítési egységétől függ:

Az IdentityTool egy adott kérésnél mindkét forrást összegyűjti és összefűzi: a kérés domain-jéhez illeszkedő orchestrátor-példányok leírását, és a ToolRegistry domain+system illesztésének megfelelő tool-példányok leírását (a getChatTools és getOrchestratorActivatedTools eredményéből). A kettő nem zárja ki egymást – egy orchestrátoros domainen belül egy önálló, ALL domain-ű tool leírása ugyanúgy bekerül az összegzésbe, mint az orchestrátoré.

// ── Orchestrátoros plugin: a teljes domain tudását az orchestrátor írja le ──
@Override
public String getIdentityDescription() {
    return """
        Rövid, felhasználóknak szóló összefoglaló arról, milyen témákban és milyen
        adatforrások alapján tud segíteni a domain – a plugin összes tool-jának
        tudását egybefoglalva.
        """;
}

// ── Önálló, orchestrátor nélküli plugin: a tool írja le saját magát ──
@Override
public String getDomain() { return IChatbotTool.ALL; }

@Override
public String getEligibleSystem() { return IChatbotTool.ALL; }

@Override
public String getIdentityDescription() {
    return """
        Rövid, önmagában értelmezhető leírás arról, mire képes ez az egy tool –
        nem tételez fel semmilyen domain-specifikus kontextust, mert bármely
        domainen, más pluginoktól függetlenül is települhet.
        """;
}
⚠️
A getIdentityDescription()-t ne keverd össze a getSystemPromptSupplement()-tel: az utóbbi a normál RAG-válaszok rendszerpromptját egészíti ki (ArangoDB-ben tárolva, admin által is szerkeszthető), és csak akkor kerül bele, ha a tool ténylegesen aktiválódott az adott kérésre. A getIdentityDescription() ezzel szemben kizárólag az identitás-kérdésekre adott válaszhoz épül fel, kódban rögzített szöveg, és attól függetlenül bekerül az összegzésbe, hogy az adott tool aktiválódott-e a beszélgetés során.

5.11   Feladatok rész-lépésekre bontása – IAgentCoordinator

Egy IOrchestratorActivatedTool az IAgentCoordinator interfészt implementálva, még a suitability teszt előtt, rész-lépésekre bonthatja a feladatot a saját algoritmusa szerint - meghatározva, mely további tool-oknak kell aktiválódniuk, és milyen sorrendben. Ez az executeOrchestratorActivatedTools() minden körében lefut, és a keletkező rész-lépés-fát rekurzívan, a feladat saját tool-aktiválása előtt dolgozza fel az orchestrátor. A teljes szerződést, a rész-lépés-fa végrehajtási szabályait és a tool-ok közötti együttműködés további csatornáit (kontextus-megosztás, garantált függőség, közvetlen hívás) az 6. fejezet tárgyalja részletesen.