SPB Git forge

spb/uqo-chat

Public
14commits 1branches 0releases
1.4 MBsize
maindefault branch
17 days agolast push
Python 64.6% TypeScript 33.7% CSS 0.8%
2.2 KB · 58 lines python
Raw Blame History
1"""search_course_content — RAG over the official course material."""23from __future__ import annotations45import re6from typing import Any78from pydantic import BaseModel, Field910from app.llm.openrouter import get_llm11from app.llm.schemas import ToolResult12from app.rag import retriever13from app.tools.registry import ToolContext, registry141516def _excerpt(text: str, limit: int = 280) -> str:17    """Plain-text excerpt for the UI card: drop LaTeX delimiters/commands and headings."""18    t = re.sub(r"\$\$?(.*?)\$\$?", lambda m: re.sub(r"\\[a-zA-Z]+|[{}]", "", m.group(1)), text, flags=re.S)19    t = re.sub(r"^#+\s*", "", t, flags=re.M).replace("**", "")20    t = re.sub(r"\s+", " ", t).strip()21    return t if len(t) <= limit else t[: limit - 1] + "…"222324class SearchCourseArgs(BaseModel):25    query: str = Field(..., min_length=2, max_length=400)26    course: str | None = Field(None, description="IMM1003, IMM1033 ou null pour les deux")27    top_k: int = Field(6, ge=1, le=10)282930async def run(args: dict[str, Any], ctx: ToolContext) -> ToolResult:31    await ctx.report("running", "Recherche dans le matériel du cours…")32    courses: set[str] | None = None33    if args.get("course"):34        courses = {str(args["course"]).upper()}35    q_emb = None36    if ctx.settings.MODEL_EMBEDDINGS and retriever.index.embeddings is not None:37        try:38            vecs = await get_llm().embed([args["query"]])39            q_emb = vecs[0] if vecs else None40        except Exception:  # noqa: BLE00141            q_emb = None42    hits = retriever.index.search(43        args["query"], top_k=args["top_k"], courses=courses, boost_course=ctx.course_code,44        include_professor=ctx.role in {"professor", "admin"}, query_embedding=q_emb)45    sources = [{46        "index": i + 1, "id": h.chunk_id, "course": h.course, "module": h.module,47        "section": h.section, "page": h.page, "url": h.url,48        "excerpt": _excerpt(h.content), "score": round(h.score, 3),49    } for i, h in enumerate(hits)]50    return ToolResult(51        content=retriever.format_for_model(hits),52        payload={"query": args["query"], "sources": sources},53        meta={"summary": f"{len(hits)} passage(s) du cours"},54    )555657registry.register("search_course_content", run, SearchCourseArgs)58