Skip to content

Knowledge Base

src.domains.knowledge_base.endpoints

REST API endpoints for Knowledge Base creation and RAG attachment to LLMs.

Atomic endpoints following REST principles. Each endpoint delegates business logic to services.

Architecture

Endpoints -> Services -> Repository -> Database

Endpoints

GET /knowledge_base/{llm_id}/status - Poll KB job status POST /knowledge_base/create - Create new KB assistant or update existing

create_knowledge_base(payload, background_tasks, db=Depends(get_db))

Create new Knowledge Base assistant or update existing one.

Decision tree: - If base LLM has NO KB attached: Create new specialized LLM + KB - If base LLM HAS KB attached: Update existing KB with new documents

A background task then ingests the documents asynchronously (extraction → chunking → embeddings → vector store); the polled status endpoint reports progress and errors.

Parameters:

Name Type Description Default
payload KnowledgeBaseCreate

Request body with selectedModel, modelName, description, paths.

required
background_tasks BackgroundTasks

FastAPI background task queue.

required
db Session

Database session.

Depends(get_db)

Returns:

Name Type Description
KnowledgeBaseResponse

msg and model_id

Raises:

Type Description
InvalidInputException

If validation fails.

KnowledgeBaseNotFoundException

If base LLM not found.

DatabaseException

If KB creation fails.

Source code in backend/src/domains/knowledge_base/endpoints.py
@router.post("/create", response_model=KnowledgeBaseResponse)
def create_knowledge_base(
    payload: KnowledgeBaseCreate, background_tasks: BackgroundTasks, db: Session = Depends(get_db)
):
    """Create new Knowledge Base assistant or update existing one.

    Decision tree:
    - If base LLM has NO KB attached: Create new specialized LLM + KB
    - If base LLM HAS KB attached: Update existing KB with new documents

    A background task then ingests the documents asynchronously (extraction →
    chunking → embeddings → vector store); the polled status endpoint reports
    progress and errors.

    Args:
        payload: Request body with selectedModel, modelName, description, paths.
        background_tasks: FastAPI background task queue.
        db: Database session.

    Returns:
        KnowledgeBaseResponse: msg and model_id

    Raises:
        InvalidInputException: If validation fails.
        KnowledgeBaseNotFoundException: If base LLM not found.
        DatabaseException: If KB creation fails.
    """
    # Validate payload
    if not payload.paths or not isinstance(payload.paths, list):
        raise InvalidInputException("paths (must be non-empty list)")

    if not payload.selectedModel:
        raise InvalidInputException("selectedModel")

    if not payload.modelName:
        raise InvalidInputException("modelName")

    logger.info(
        f"KB creation request: base_llm={payload.selectedModel}, "
        f"name={payload.modelName}, files={len(payload.paths)}"
    )

    service = KB_Service()

    try:
        # Check if base LLM has existing KB
        from src.domains.knowledge_base.repository import KB_Repository

        repo = KB_Repository()
        base_llm = repo.get_local_llm_by_id(db, payload.selectedModel)

        if not base_llm:
            raise KnowledgeBaseNotFoundException(payload.selectedModel)

        if base_llm.is_attached_to_kb:
            # Update existing KB
            logger.info(f"Updating existing KB for LLM {base_llm.id}")

            llm_id, kb_job_id = service.update_existing_kb(
                db=db, base_llm_id=base_llm.id, file_paths=payload.paths
            )

            # Queue background task for update
            background_tasks.add_task(
                _run_kb_update_task, kb_job_id=kb_job_id, file_paths=payload.paths
            )

            return KnowledgeBaseResponse(
                msg="Knowledge Base is being updated with new documents.", model_id=llm_id
            )

        else:
            # Create new KB assistant. Refuse a name already carried by any
            # installed model (#317): two identical names are indistinguishable
            # in every picker. The update path above never reads the name, so
            # updating an assistant with its own (existing) name stays valid.
            duplicate = repo.get_local_llm_by_name(db, payload.modelName)
            if duplicate:
                raise StateConflictException(
                    f"A local model named '{payload.modelName.strip()}' already "
                    "exists. Choose a different assistant name.",
                    status_code=http_status.HTTP_409_CONFLICT,
                )

            logger.info(f"Creating new KB assistant from base LLM {base_llm.id}")

            llm_id, kb_job_id = service.create_kb_assistant(
                db=db,
                base_llm_id=base_llm.id,
                model_name=payload.modelName,
                description=payload.description or "",
                file_paths=payload.paths,
            )

            # Queue background task for creation
            background_tasks.add_task(
                _run_kb_creation_task, kb_job_id=kb_job_id, file_paths=payload.paths
            )

            return KnowledgeBaseResponse(
                msg="Knowledge Base Assistant is being created.", model_id=llm_id
            )

    except AppBaseException:
        raise

    except ValueError as e:
        logger.error(f"Validation error: {e}")
        raise KnowledgeBaseNotFoundException(payload.selectedModel)

    except Exception as e:
        raise DatabaseException("Error creating Knowledge Base Assistant", trace=str(e))

get_embedding_model_status()

On-disk presence of the KB embedding model + any in-flight download.

Source code in backend/src/domains/knowledge_base/endpoints.py
@router.get("/embedding-model/status")
def get_embedding_model_status():
    """On-disk presence of the KB embedding model + any in-flight download."""
    return download_state()

get_kb_job_status(llm_id, db=Depends(get_db))

Poll Knowledge Base job status with automatic cleanup for failed jobs.

Parameters:

Name Type Description Default
llm_id int

Specialized LLM ID created during KB creation.

required
db Session

Database session.

Depends(get_db)

Returns:

Name Type Description
dict

status, status_updated_at, error_message

Raises:

Type Description
KnowledgeBaseNotFoundException

If KB job not found.

DatabaseException

If error fetching status.

Source code in backend/src/domains/knowledge_base/endpoints.py
@router.get("/{llm_id}/status")
def get_kb_job_status(llm_id: int, db: Session = Depends(get_db)):
    """Poll Knowledge Base job status with automatic cleanup for failed jobs.

    Args:
        llm_id: Specialized LLM ID created during KB creation.
        db: Database session.

    Returns:
        dict: status, status_updated_at, error_message

    Raises:
        KnowledgeBaseNotFoundException: If KB job not found.
        DatabaseException: If error fetching status.
    """
    service = KB_Service()

    try:
        status_data = service.get_kb_job_status(db, llm_id)
        return status_data

    except ValueError:
        # Polled by the UI: an absent job is a 404 the handler logs at INFO.
        raise KnowledgeBaseNotFoundException(llm_id)

    except Exception as e:
        raise DatabaseException("Error fetching KB job status", trace=str(e))

post_embedding_model_download()

Kick off the embedding-model download in the background (idempotent).

Source code in backend/src/domains/knowledge_base/endpoints.py
@router.post("/embedding-model/download")
def post_embedding_model_download():
    """Kick off the embedding-model download in the background (idempotent)."""
    return start_download()

src.domains.knowledge_base.schemas

Pydantic validation schemas for Knowledge Base creation and status responses.

Defines data transfer objects for Knowledge Base assistant creation workflow. Schemas validate document paths, base model selection, and assistant metadata.

Example

from src.domains.knowledge_base.schemas import KnowledgeBaseCreate import requests

payload = KnowledgeBaseCreate( paths=["/uploads/report1.pdf", "/uploads/report2.txt"], selectedModel=42, modelName="Financial Assistant", description="Q1-Q4 2024 earnings reports" ) response = requests.post("/knowledge_base/create", json=payload.model_dump())

KnowledgeBaseCreate

Bases: BaseModel

Request schema for creating or updating a Knowledge Base assistant.

Attributes:

Name Type Description
paths List[str]

List of absolute file paths to PDF/TXT documents to ingest.

selectedModel int

Database ID of base LLM to specialize with KB attachment.

modelName str

Name for the new specialized assistant (e.g., "Financial Reports Bot").

description str

Optional user annotation describing the KB's purpose or domain.

Example

kb_req = KnowledgeBaseCreate( ... paths=["/uploads/q1.pdf", "/uploads/q2.pdf"], ... selectedModel=42, ... modelName="Earnings Reports Assistant", ... description="2024 Q1-Q4 financial data" ... )

KnowledgeBaseResponse

Bases: BaseModel

Response schema for KB creation endpoint with async processing confirmation.

Attributes:

Name Type Description
msg str

Human-readable status message (e.g., "Knowledge Base Assistant is being created.").

model_id int

Database ID of the newly created specialized LLM (poll /kb/{model_id}/status).

Example

response = KnowledgeBaseResponse( ... msg="Knowledge Base Assistant is being created.", ... model_id=108 ... )

Client polls GET /knowledge_base/108/status until status="completed"

src.domains.knowledge_base.services

Business logic layer for Knowledge Base domain.

Implements service pattern for Knowledge Base operations: KB/assistant lifecycle, job state machine, and background ingestion orchestration.

Ingestion pipeline (per file, background task): SHA-256 → dedup against the KB's KnowledgeDocument rows → register the document → extract (DocumentReader, services stay format-agnostic) → pending_vision stops here (image/scanned PDF, indexed by the OCR/VLM tiers of a later release) → 3-pass chunking → hybrid vector store (rag.kb_chunks). Per-file failures mark THAT document failed and the run continues; the job only fails when nothing could be ingested.

Classes:

Name Description
KB_Service

Business logic for KB creation, updates, and job tracking.

Architecture

Endpoints → Services → Repository → Database ↓ src.ingestion (DocumentReader → chunking → vector store)

KB_Service()

Business logic for Knowledge Base operations.

Coordinates KB creation, updates, document processing, and background tasks.

Source code in backend/src/domains/knowledge_base/services.py
def __init__(self):
    self.repo = KB_Repository()
    self.reader = DocumentReader()

create_kb_assistant(db, base_llm_id, model_name, description, file_paths)

Create new Knowledge Base assistant (database setup only).

Creates specialized LLM, KnowledgeBase, and KBJob entities. Does NOT process documents — that's done in the background task.

Parameters:

Name Type Description Default
db Session

Database session.

required
base_llm_id int

Base LLM ID to specialize from.

required
model_name str

Name for specialized assistant.

required
description str

Description for specialized assistant.

required
file_paths List[str]

List of file paths to process.

required

Returns:

Type Description
Tuple[int, int]

Tuple of (specialized_llm_id, kb_job_id) for background task.

Raises:

Type Description
ValueError

If base LLM not found or not local.

Source code in backend/src/domains/knowledge_base/services.py
def create_kb_assistant(
    self,
    db: Session,
    base_llm_id: int,
    model_name: str,
    description: str,
    file_paths: List[str],
) -> Tuple[int, int]:  # Returns (llm_id, kb_job_id)
    """Create new Knowledge Base assistant (database setup only).

    Creates specialized LLM, KnowledgeBase, and KBJob entities. Does NOT
    process documents — that's done in the background task.

    Args:
        db: Database session.
        base_llm_id: Base LLM ID to specialize from.
        model_name: Name for specialized assistant.
        description: Description for specialized assistant.
        file_paths: List of file paths to process.

    Returns:
        Tuple of (specialized_llm_id, kb_job_id) for background task.

    Raises:
        ValueError: If base LLM not found or not local.
    """
    # Validate base LLM
    base_llm = self.repo.get_local_llm_by_id(db, base_llm_id)
    if not base_llm:
        raise ValueError(f"Base LLM {base_llm_id} not found or not local")

    # Create Knowledge Base
    kb = self.repo.create_knowledge_base(db)

    # Create specialized LLM
    specialized_llm = self.repo.create_specialized_llm(
        db=db, name=model_name, description=description, base_llm=base_llm, kb_id=kb.id
    )

    # Create KBJob
    kb_job = self.repo.create_kb_job(
        db=db,
        base_model_id=base_llm.id,
        new_model_id=specialized_llm.id,
        kb_id=kb.id,
        status="pending",
    )

    db.commit()
    logger.info(
        f"Created KB assistant setup: LLM={specialized_llm.id}, " f"KB={kb.id}, Job={kb_job.id}"
    )

    return specialized_llm.id, kb_job.id

get_kb_job_status(db, llm_id)

Get KB job status with automatic cleanup for failed jobs.

Parameters:

Name Type Description Default
db Session

Database session.

required
llm_id int

Specialized LLM ID (new_model_id in KBJob).

required

Returns:

Type Description
Dict[str, Any]

Dict with status, updated_at, and error_message.

Raises:

Type Description
ValueError

If KB job not found.

Source code in backend/src/domains/knowledge_base/services.py
def get_kb_job_status(self, db: Session, llm_id: int) -> Dict[str, Any]:
    """Get KB job status with automatic cleanup for failed jobs.

    Args:
        db: Database session.
        llm_id: Specialized LLM ID (new_model_id in KBJob).

    Returns:
        Dict with status, updated_at, and error_message.

    Raises:
        ValueError: If KB job not found.
    """
    kb_job = self.repo.get_kb_job_by_model_id(db, llm_id)
    if not kb_job:
        raise ValueError(f"KB job not found for LLM ID {llm_id}")

    # Cleanup failed jobs -- but ONLY failed creations. An update job
    # shares its new_model_id with base_model_id (see update_existing_kb);
    # a creation job's new_model_id is a freshly-minted specialized LLM
    # distinct from its base. Deleting on any failure would wipe out an
    # already-working assistant just because a LATER update to it failed
    # (e.g. one bad file) -- the assistant and its existing KB content
    # must survive; only the half-built creation has nothing to lose.
    if kb_job.status == "failed" and kb_job.base_model_id != kb_job.new_model_id:
        self._cleanup_failed_job(db, llm_id, kb_job)

    return {
        "status": kb_job.status,
        "status_updated_at": kb_job.updated_at,
        "error_message": kb_job.error_message if kb_job.status == "failed" else None,
    }

process_and_index_documents(db, kb_job_id, file_paths, is_update=False)

Ingest documents into the KB's vector store (background task).

Per-file pipeline — see module docstring. Each file commits on its own: the document row must be VISIBLE to the vector store's separate connection (FK), and progress survives an interruption.

Parameters:

Name Type Description Default
db Session

Database session (a fresh SessionLocal in background tasks).

required
kb_job_id int

KBJob ID to track progress.

required
file_paths List[str]

List of file paths to process.

required
is_update bool

True if updating existing KB, False if creating new.

False
Source code in backend/src/domains/knowledge_base/services.py
def process_and_index_documents(
    self, db: Session, kb_job_id: int, file_paths: List[str], is_update: bool = False
) -> None:
    """Ingest documents into the KB's vector store (background task).

    Per-file pipeline — see module docstring. Each file commits on its
    own: the document row must be VISIBLE to the vector store's separate
    connection (FK), and progress survives an interruption.

    Args:
        db: Database session (a fresh SessionLocal in background tasks).
        kb_job_id: KBJob ID to track progress.
        file_paths: List of file paths to process.
        is_update: True if updating existing KB, False if creating new.
    """
    start_s = time.perf_counter()
    try:
        kb_job = self.repo.get_kb_job_by_id(db, kb_job_id)
        if not kb_job:
            raise ValueError(f"KBJob {kb_job_id} not found")

        self.repo.update_kb_job_status(db, kb_job, "running")

        kb = self.repo.get_knowledge_base_by_id(db, kb_job.kb_id)
        if not kb:
            raise ValueError(f"KnowledgeBase {kb_job.kb_id} not found")

        logger.info(
            f"KB job {kb_job_id} started "
            f"({'update' if is_update else 'creation'}, kb_id={kb.id}, "
            f"{len(file_paths)} file(s))"
        )

        indexed, pending, skipped, empty, failed = 0, 0, 0, 0, 0
        for raw_path in file_paths:
            outcome = self._ingest_one_file(db, kb.id, raw_path)
            if outcome == "indexed":
                indexed += 1
            elif outcome == "pending_vision":
                pending += 1
            elif outcome == "skipped":
                skipped += 1
            elif outcome == "empty":
                empty += 1
            else:
                failed += 1

        duration_ms = (time.perf_counter() - start_s) * 1000
        logger.info(
            f"KB job {kb_job_id} ({'update' if is_update else 'creation'}): "
            f"{indexed} indexed, {pending} pending_vision, "
            f"{skipped} duplicates, {empty} empty, {failed} failed, "
            f"duration_ms={duration_ms:.0f}"
        )

        # Don't report success when nothing queryable was added. A batch that
        # indexed zero chunks and had no duplicates (its files were empty,
        # pending_vision with no OCR tier, or failed) is a failure the user must
        # see — never a silent "completed". All-duplicates (skipped > 0) stays a
        # success: that content is already in the KB.
        if indexed == 0 and skipped == 0:
            raise ValueError(
                f"No searchable content could be indexed "
                f"({failed} failed, {empty} empty, {pending} pending vision)."
            )

        self.repo.update_kb_job_status(db, kb_job, "completed")

    except Exception as e:
        # Marked failed for the polling UI and re-raised: the background
        # task that owns this job logs the record, with the traceback.
        self._handle_indexing_error(db, kb_job_id, str(e))
        raise

update_existing_kb(db, base_llm_id, file_paths)

Queue update for existing KB with new documents (database setup only).

Creates KBJob for update operation. Does NOT process documents - that's done in background task.

Parameters:

Name Type Description Default
db Session

Database session.

required
base_llm_id int

Base LLM ID with existing KB attachment.

required
file_paths List[str]

List of new file paths to add.

required

Returns:

Type Description
Tuple[int, int]

Tuple of (base_llm_id, kb_job_id) for background task.

Raises:

Type Description
ValueError

If LLM not found or KB not attached.

Source code in backend/src/domains/knowledge_base/services.py
def update_existing_kb(
    self, db: Session, base_llm_id: int, file_paths: List[str]
) -> Tuple[int, int]:  # Returns (llm_id, kb_job_id)
    """Queue update for existing KB with new documents (database setup only).

    Creates KBJob for update operation. Does NOT process documents - that's
    done in background task.

    Args:
        db: Database session.
        base_llm_id: Base LLM ID with existing KB attachment.
        file_paths: List of new file paths to add.

    Returns:
        Tuple of (base_llm_id, kb_job_id) for background task.

    Raises:
        ValueError: If LLM not found or KB not attached.
    """
    base_llm = self.repo.get_local_llm_by_id(db, base_llm_id)
    if not base_llm:
        raise ValueError(f"Base LLM {base_llm_id} not found or not local")

    if not base_llm.is_attached_to_kb or not base_llm.kb_id:
        raise ValueError(f"LLM {base_llm_id} is not attached to a KB")

    kb = self.repo.get_knowledge_base_by_id(db, base_llm.kb_id)
    if not kb:
        raise ValueError(f"KB {base_llm.kb_id} not found for LLM {base_llm_id}")

    # Create update job
    kb_job = self.repo.create_kb_job(
        db=db,
        base_model_id=base_llm.id,
        new_model_id=base_llm.id,  # Same LLM for updates
        kb_id=kb.id,
        status="pending",
    )

    db.commit()
    logger.info(f"Created KB update job: Job={kb_job.id}, KB={kb.id}")

    return base_llm.id, kb_job.id