import logging import traceback import gradio as gr from app.core.config import settings from app.ui.theme import CSS, HEAD, JS from app.utils.zerogpu import gpu logger = logging.getLogger(__name__) THEME = gr.themes.Base( primary_hue="teal", secondary_hue="yellow", neutral_hue="stone", radius_size="sm", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], ) # Fixed pipeline constants CHUNK_SIZE = 1200 CHUNK_OVERLAP = 200 RETRIEVE_K = 3 def _format_metadata(metadata: dict) -> str: if not metadata: return "No metadata found." rows = [] for key, value in metadata.items(): rows.append(f"**{key}**: {value}") return "\n\n".join(rows) @gpu() def _ingest( url: str, pdf_file: str | None, collection_name: str, ): logger.info( "Ingest requested url=%s pdf_file=%s chunk_size=%s chunk_overlap=%s collection=%s", url, pdf_file, CHUNK_SIZE, CHUNK_OVERLAP, collection_name, ) try: from app.services.ingestion import ingest_source result = ingest_source( url=url, pdf_path=pdf_file, chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, collection_name=collection_name, ) document = result.document status = ( f"### Ingestion complete\n\n" f"Uploaded **{len(result.chunks)} chunks** into Qdrant collection " f"`{result.collection_name}`.\n\n" f"Saved extracted text to `{result.export_path}`." ) preview = document.text[:12000] if len(document.text) > len(preview): preview += "\n\n[Preview truncated in UI. Full text is saved in the export file.]" return ( status, document.title, document.source_type.value, str(len(document.text)), str(len(result.chunks)), _format_metadata(document.metadata), preview, str(result.export_path), ) except Exception as exc: return ( f"### Ingestion failed\n\n`{type(exc).__name__}: {exc}`\n\n```text\n{traceback.format_exc(limit=2)}\n```", "", "", "0", "0", "", "", "", ) @gpu() def _search(query: str, collection_name: str): logger.info("Search requested query=%s limit=%s collection=%s", query, RETRIEVE_K, collection_name) try: from app.services.ingestion import search_knowledge_base results = search_knowledge_base(query, limit=RETRIEVE_K, collection_name=collection_name) except Exception as exc: if "MPS backend out of memory" in str(exc): return ( "### Search failed\n\n" "The local embedding model ran out of Apple GPU memory. " "Restart the app so the new CPU embedding setting takes effect. " "Keep `EMBEDDING_DEVICE=cpu` in `.env`." ) return f"### Search failed\n\n`{type(exc).__name__}: {exc}`" if not results: return "No matches found." blocks = [] for index, result in enumerate(results, start=1): excerpt = result.text[:1200] blocks.append( "\n".join( [ f"### {index}. {result.title}", f"**Score:** {result.score:.4f}", f"**Source:** {result.source_type} | {result.source}", "", excerpt, ] ) ) return "\n\n---\n\n".join(blocks) @gpu() def _answer(query: str, collection_name: str): logger.info("Answer requested query=%s limit=%s collection=%s", query, RETRIEVE_K, collection_name) try: from app.services.ingestion import answer_from_knowledge_base result = answer_from_knowledge_base(query, limit=RETRIEVE_K, collection_name=collection_name) except Exception as exc: if "MPS backend out of memory" in str(exc): return ( "### Answer failed\n\n" "The local embedding model ran out of Apple GPU memory. " "Restart the app so the new CPU embedding setting takes effect. " "Keep `EMBEDDING_DEVICE=cpu` in `.env`.", "", "", ) return f"### Answer failed\n\n`{type(exc).__name__}: {exc}`", "", "" context_blocks = [] for index, item in enumerate(result.context, start=1): context_blocks.append( "\n".join( [ f"### [{index}] {item.title}", f"**Score:** {item.score:.4f}", f"**Source:** {item.source_type} | {item.source}", "", item.text[:1000], ] ) ) reasoning = result.reasoning or "No reasoning content was returned by the API." return result.answer, reasoning, "\n\n---\n\n".join(context_blocks) def build_app() -> gr.Blocks: with gr.Blocks( title=f"{settings.PROJECT_NAME} Ingestor", ) as demo: with gr.Column(elem_id="kh-shell"): # Room Tag badge inside the chalkboard frame gr.HTML( f'
{settings.NEMOTRON_EMBED_MODEL}{settings.NEMOTRON_PARSE_MODEL}{settings.NVIDIA_CHAT_MODEL}{settings.QDRANT_COLLECTION_NAME}