CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
dashboard_chat_contracts.py300 linesDownload Raw Back to hackathon_advisor
1"""Tool contracts for the atlas chat: native MiniCPM5 format, chat-specific fallback.2 3The dashboard chat drives the BASE MiniCPM5-1B model through its native4tool-calling protocol: tool JSON schemas go in via ``apply_chat_template(...,5tools=...)`` and the model answers either with plain prose (no tool needed) or6with one XML call of the form::7 8    <function name="tool_name"><param name="arg">value</param></function>9 10That argument encoding (``<param>`` children, CDATA for special characters)11differs from the advisor's JSON-body format in ``tool_contracts.py``, so it gets12its own parser here. Validation reuses ``validate_tool_call`` against the chat13tool specs. The degradation ladder is chat-specific: prose with no function call14is a deliberate "no tool" outcome, while a malformed call degrades through a15keyword intent router and finally to a BM25 search of the raw message — never to16the advisor's ``search_projects``/``find_whitespace`` defaults, which assume the17advisor's idea-board context.18"""19 20from __future__ import annotations21 22from dataclasses import dataclass23import re24from typing import Any, Literal25from xml.etree import ElementTree26 27from hackathon_advisor.tool_contracts import (28    ToolCall,29    ToolContractError,30    ToolField,31    ToolSpec,32    validate_tool_call,33)34 35CHAT_TOOL_SPECS: dict[str, ToolSpec] = {36    "atlas_overview": ToolSpec(37        name="atlas_overview",38        description="Summarize the whole field: project totals, biggest clusters, quest coverage.",39        fields={},40    ),41    "list_clusters": ToolSpec(42        name="list_clusters",43        description="List the project clusters (themes) with sizes and keywords.",44        fields={},45    ),46    "show_cluster": ToolSpec(47        name="show_cluster",48        description="Inspect one cluster by its label and show example projects.",49        fields={50            "label": ToolField("string", "Cluster label, such as Voice / Chatbot.", required=True)51        },52    ),53    "list_quests": ToolSpec(54        name="list_quests",55        description="List the hackathon quests with how many projects completed each.",56        fields={},57    ),58    "show_quest": ToolSpec(59        name="show_quest",60        description="Inspect one quest: its description, coverage, and example projects.",61        fields={62            "quest": ToolField(63                "string", "Quest name, such as Off the Grid or Tiny Titan.", required=True64            )65        },66    ),67    "show_project": ToolSpec(68        name="show_project",69        description="Read one project's README and main app file by project name.",70        fields={"project": ToolField("string", "Project name, id, or slug.", required=True)},71    ),72    "top_projects_by_quests": ToolSpec(73        name="top_projects_by_quests",74        description="Rank projects by how many quests they completed (the quest leaderboard).",75        fields={},76    ),77    "search_projects": ToolSpec(78        name="search_projects",79        description="Full-text search across all projects on the map.",80        fields={81            "query": ToolField("string", "Topic, model, or idea to search for.", required=True)82        },83    ),84    "recent_activity": ToolSpec(85        name="recent_activity",86        description="Show the most recently updated projects.",87        fields={},88    ),89}90 91 92@dataclass(frozen=True)93class ChatToolResolution:94    """Outcome of reading one pass-1 model output.95 96    ``none`` means the model deliberately answered without a tool (chit-chat);97    ``call`` is None only in that case.98    """99 100    status: Literal["valid", "defaulted", "none"]101    call: ToolCall | None102    errors: tuple[str, ...]103 104    def to_dict(self) -> dict[str, Any]:105        return {106            "status": self.status,107            "call": self.call.to_dict() if self.call else None,108            "errors": list(self.errors),109        }110 111 112def chat_tool_schemas() -> list[dict[str, Any]]:113    return [spec.to_schema() for spec in CHAT_TOOL_SPECS.values()]114 115 116_FUNCTION_BLOCK_RE = re.compile(r"<function\b.*?</function>", re.DOTALL)117_FUNCTION_OPEN_RE = re.compile(r"<function\b")118 119 120def parse_native_tool_call(text: str) -> ToolCall:121    """Extract and parse the first native-format function call in ``text``.122 123    The native template lets the model wrap a call in prose, so surrounding text124    is ignored; only the ``<function ...>...</function>`` block is parsed.125    """126    block = _FUNCTION_BLOCK_RE.search(text or "")127    if block is None:128        raise ToolContractError("no <function> call found in model output")129    try:130        node = ElementTree.fromstring(block.group(0))131    except ElementTree.ParseError as error:132        raise ToolContractError(f"invalid native tool call XML: {error}") from error133    name = str(node.attrib.get("name") or "").strip()134    if not name:135        raise ToolContractError("function call is missing a name")136    arguments: dict[str, Any] = {}137    for child in node:138        if child.tag != "param":139            raise ToolContractError(f"unexpected element <{child.tag}> in function call")140        param_name = str(child.attrib.get("name") or "").strip()141        if not param_name:142            raise ToolContractError("param is missing a name")143        arguments[param_name] = _element_text(child).strip()144    return ToolCall(name=name, arguments=arguments)145 146 147def resolve_chat_tool_call(model_output: str, fallback_query: str = "") -> ChatToolResolution:148    """Validate one pass-1 output, or degrade: intent router, then BM25 search."""149    text = str(model_output or "")150    if _FUNCTION_OPEN_RE.search(text) is None:151        return ChatToolResolution(status="none", call=None, errors=())152 153    errors: list[str] = []154    try:155        call = validate_tool_call(parse_native_tool_call(text), specs=CHAT_TOOL_SPECS)156        return ChatToolResolution(status="valid", call=call, errors=())157    except ToolContractError as error:158        errors.append(str(error))159 160    call = heuristic_chat_call(fallback_query)161    return ChatToolResolution(status="defaulted", call=call, errors=tuple(errors))162 163 164def data_intent_call(message: str) -> ToolCall | None:165    """Map a message with a CLEAR data intent to a tool call; None means no clear intent.166 167    Used as the accuracy backstop when the model answers a data-shaped question in plain168    prose: an explicit intent routes to the matching tool, anything else (greetings,169    meta questions) stays conversational."""170    lower = " ".join(str(message or "").casefold().split())171    cleaned = " ".join(str(message or "").split())172    detail_intent = _mentions(173        lower, ("what is in", "what's in", "inside", "show me the", "tell me about", "about the")174    )175    if _mentions(176        lower, ("leaderboard", "most quest", "who completed", "top project", "top team", "winning")177    ):178        return ToolCall("top_projects_by_quests", {})179    if _mentions(lower, ("cluster", "theme", "group", "region")):180        if detail_intent:181            # cluster_detail() fuzzy-resolves a label embedded in the question.182            return ToolCall("show_cluster", {"label": cleaned})183        return ToolCall("list_clusters", {})184    if _mentions(lower, ("quest", "badge", "challenge")):185        if detail_intent:186            return ToolCall("show_quest", {"quest": cleaned})187        return ToolCall("list_quests", {})188    if _mentions(lower, ("recent", "latest", "newest", "just updated", "activity")):189        return ToolCall("recent_activity", {})190    if _mentions(191        lower,192        (193            "overview",194            "everyone building",195            "everyone doing",196            "whole field",197            "summary of the",198            "most liked",199            "most popular",200            "coolest",201            "best project",202            "favorite project",203        ),204    ):205        return ToolCall("atlas_overview", {})206    if (207        _mentions(208            lower,209            ("readme", "app file", "source code", "how does", "how is", "what does", "built with"),210        )211        or detail_intent212    ):213        # project_detail() spots a title embedded in the question; the engine falls214        # back to BM25 search when no project matches.215        return ToolCall("show_project", {"project": cleaned})216    if _mentions(217        lower,218        (219            "find ",220            "search",221            "looking for",222            "projects about",223            "projects on",224            "show me",225            "anything about",226            "who is building",227            "how many",228            "number of",229            "count of",230            "is there a",231            "are there any",232        ),233    ):234        return ToolCall("search_projects", {"query": " ".join(str(message).split())})235    return None236 237 238_SMALLTALK_PATTERNS = (239    "hi",240    "hello",241    "hey",242    "yo",243    "thanks",244    "thank you",245    "ok",246    "okay",247    "cool",248    "nice",249    "bye",250    "goodbye",251    "why",252    "really",253    "are you sure",254    "who are you",255    "what are you",256    "what can you do",257    "how do you work",258    "help",259)260 261 262def smalltalk_intent(message: str) -> bool:263    """True only for greetings, meta questions, and short follow-ups.264 265    The chat is a data-exploration surface, so the safe default for anything266    substantive is a tool (BM25 search) — letting an unmatched question fall267    through to ungrounded small talk is how the model ends up inventing facts."""268    lower = " ".join(str(message or "").casefold().split()).rstrip(".!?")269    if not lower:270        return True271    # Pattern-table only: an unknown two-word phrase like "knitting helpers" is a272    # search, and an unmatched search honestly answers "no match" — never invents.273    return any(274        lower == pattern or lower.startswith(f"{pattern} ") for pattern in _SMALLTALK_PATTERNS275    )276 277 278def heuristic_chat_call(message: str) -> ToolCall:279    """Keyword intent router used when the model's tool call cannot be salvaged."""280    intent = data_intent_call(message)281    if intent is not None:282        return intent283    cleaned = " ".join(str(message or "").split())284    if cleaned:285        return ToolCall("search_projects", {"query": cleaned})286    return ToolCall("atlas_overview", {})287 288 289def strip_function_blocks(text: str) -> str:290    """Remove any stray function-call XML a pass-2 generation might emit."""291    return _FUNCTION_BLOCK_RE.sub("", str(text or "")).strip()292 293 294def _mentions(lower_text: str, phrases: tuple[str, ...]) -> bool:295    return any(phrase in lower_text for phrase in phrases)296 297 298def _element_text(node: ElementTree.Element) -> str:299    return "".join(node.itertext())300