Python 64.6%
TypeScript 33.7%
CSS 0.8%
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