import { ClsService } from "nestjs-cls"; import { ResponderService } from "../../../agents/responder/services/responder.service"; import { JsonApiDataInterface } from "../../../core/jsonapi/interfaces/jsonapi.data.interface"; import { JsonApiService } from "../../../core/jsonapi/services/jsonapi.service"; import { AbstractService } from "../../../core/neo4j/abstracts/abstract.service"; import { HandbookThread, HandbookThreadDescriptor } from "../entities/handbook-thread"; import { HandbookPageRepository } from "../repositories/handbook-page.repository"; import { HandbookThreadMessageRepository } from "../repositories/handbook-thread-message.repository"; import { HandbookThreadRepository } from "../repositories/handbook-thread.repository"; import { HandbookThreadMessageService } from "./handbook-thread-message.service"; /** How many prior turns are replayed to the responder as history. */ export declare const HANDBOOK_MAX_MESSAGES_TO_LLM = 20; /** Longest auto-generated title, when the responder supplies none. */ export declare const HANDBOOK_TITLE_MAX_LENGTH = 60; /** * Persisted handbook conversations. * * The handbook chat deliberately does NOT go through the package `Assistant`. * `Assistant` is `isCompanyScoped: true`, and a platform administrator — the * only user this feature is for — has no Company, so a company-scoped thread * would 404 for exactly that user. Company scoping stays strict; the thread * lives here instead, global and owner-scoped. * * That is why `companyId` is read off CLS and passed through EXACTLY as found, * `undefined` included. It is not defaulted, substituted or looked up. * `ResponderService.run` types it `string | undefined`; token accounting * already tolerates a company-less turn (TokenUsageRepository makes the * `BELONGS_TO` edge conditional); and handbook retrieval never consults a * company — the handbook branch of the source-query provider matches * `(:HandbookPage)` and ignores tenancy altogether. */ export declare class HandbookThreadService extends AbstractService { private readonly threads; private readonly responder; private readonly messages; private readonly messageRepository; private readonly pages; protected readonly descriptor: import("../../..").EntityDescriptor; private readonly threadLogger; constructor(jsonApiService: JsonApiService, threads: HandbookThreadRepository, clsService: ClsService, responder: ResponderService, messages: HandbookThreadMessageService, messageRepository: HandbookThreadMessageRepository, pages: HandbookPageRepository); /** * Start a thread from its first question: create the thread, run the turn, * persist both messages, and title the thread from the answer. * * The thread node is created FIRST — before the responder runs — so the two * messages have a parent to hang from, exactly as * `AssistantService.createWithFirstMessage` does. It is created with the * fallback title and retitled once the answer exists, because the responder's * title is only known after the turn. */ createWithFirstMessage(params: { question: string; /** Optional: scopes retrieval to one page. See `runTurn`. */ handbookPageId?: string; }): Promise; /** * Append a question to an existing thread and answer it with the thread's * prior messages as history. * * Returns the two NEW messages as a JSON:API list — the shape * `AssistantController.append` returns — so the client appends rather than * re-renders. The full thread is one `GET /handbookthreads/:id` away. */ appendMessage(params: { threadId: string; question: string; /** Optional: scopes retrieval to one page. See `runTurn`. */ handbookPageId?: string; }): Promise; /** * One thread with every message it holds, in position order. * * The messages ride in `included` through the descriptor's `messages` * relationship — the same traversal that lets `AssistantController` return an * assistant with its messages — so a client renders a selected thread in one * call. They are sorted here because a Cypher `OPTIONAL MATCH` collects * related nodes in no particular order. */ findThreadWithMessages(params: { threadId: string; }): Promise; /** * Rename. Overridden ONLY to close an ordering hole in the inherited * implementation: `AbstractService.patch` writes first and reads back * afterwards, and `AbstractRepository.patch` matches on `id` alone — it never * applies `buildUserHasAccess`. Without the read below, one administrator * could rename another's thread and merely be refused the response. */ patch(params: { id: string; [key: string]: any; }): Promise; /** * Delete the thread and, through the repository override, its messages. * * Overridden because the inherited `AbstractService.delete` guards ownership * with a COMPANY comparison — `(companyId ?? "") !== entity.company?.id`. A * `HandbookThread` is `isCompanyScoped: false`, so `entity.company` is always * undefined, and an administrator has no `companyId`: the comparison reduces * to `"" !== undefined`, which is true, and EVERY delete would 403. The owner * edge is the boundary here, and `readOwnedThread` is what enforces it. */ delete(params: { id: string; }): Promise; /** * Read a thread through the owner-scoped `findById`. A thread belonging to * another user raises 403 from `_validateForbidden`; an id that matches * nothing comes back null and becomes a 404 here. */ private readOwnedThread; /** * Run one handbook turn on the responder and resolve its citations to page * paths. * * `userModuleIds: []` is not a stub: the documentation branch never runs the * graph or the planner, and those two are the only readers of that list. * * `handbookPageId` is forwarded as `limitToHandbookPageId`, which the * handbook source-query provider already honours by adding * `WHERE data.id = $limitToHandbookPageId` to the `(:HandbookPage)` match. * Passed through EXACTLY as received, `undefined` included: an absent value * means retrieval spans the whole manual, which is the default a question * asked while reading one page usually wants. */ private runTurn; /** Persist one message under a thread. Returns the new message's id. */ private writeMessage; /** Every message of a thread, oldest first. */ private loadMessages; /** * Sort by `position` ascending. Applied even to results that were queried * with an ORDER BY: the ordering clause sits ahead of the relationship * traversal `buildReturnStatement()` appends, so it is not guaranteed to * survive into the rows. Position is the only ordering the client can rely * on, so it is asserted where the list is produced. */ private byPosition; /** * Title for a thread whose answer produced none: the question, trimmed to * 60 characters on a word boundary. Mirrors `AssistantService.autoTitle`. */ private fallbackTitle; } //# sourceMappingURL=handbook-thread.service.d.ts.map