HF-Knight / llm.py
knightscode139's picture
Upload folder using huggingface_hub
75bb597 verified
Raw
History Blame Contribute Delete
2.99 kB
"""LLM client: thin wrapper around llama-cpp-python (local Qwen2.5).
Loads the GGUF once and exposes chat(messages, tools) -> raw model text.
Tools are rendered into the prompt by the model's OWN chat template (so the
format matches what Qwen was trained on); we parse the <tool_call> output
ourselves in the next step, because llama-cpp's Qwen tool-call parsing is
unreliable -- it sometimes leaves the tag in the raw content instead of
filling message["tool_calls"].
"""
from __future__ import annotations
import json
import os
import re
from collections.abc import Mapping, Sequence
from typing import Any, cast
from llama_cpp import CreateChatCompletionResponse, Llama, LlamaGrammar
from config import MODEL_FILE, MODEL_PATH, MODEL_REPO, N_CTX, N_GPU_LAYERS, NARRATOR_TEMP
def _resolve_model(path: str | None) -> str:
"""Local GGUF if present (dev), else pull it from the Hub repo and cache (Space)."""
path = path or MODEL_PATH
if os.path.exists(path):
return path
from huggingface_hub import hf_hub_download
return hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
class LLM:
def __init__(self, model_path: str | None = None, n_ctx: int = N_CTX):
self.llm = Llama(
model_path=_resolve_model(model_path),
n_gpu_layers=N_GPU_LAYERS,
n_ctx=n_ctx,
verbose=False,
)
def chat(self, messages: Sequence[Mapping[str, Any]],
tools: Sequence[Mapping[str, Any]] | None = None,
temperature: float = NARRATOR_TEMP, max_tokens: int = 512,
grammar: str | None = None) -> str:
"""Return the model's raw text reply (may contain <tool_call> tags).
Public API takes plain message/tool dicts (and a GBNF grammar as plain
text); we cast once here at the llama-cpp boundary (its stubs use stricter
TypedDict unions) and build the provider-specific LlamaGrammar here too, so
callers never import llama-cpp.
"""
out = cast(
CreateChatCompletionResponse,
self.llm.create_chat_completion(
messages=cast(Any, messages),
tools=cast(Any, tools),
temperature=temperature,
max_tokens=max_tokens,
grammar=LlamaGrammar.from_string(grammar) if grammar else None,
),
)
return out["choices"][0]["message"]["content"] or ""
_TOOL_CALL_RE = re.compile(r"<tool_call>\s*(.*?)\s*</tool_call>", re.DOTALL)
def parse_tool_calls(text: str) -> list[dict]:
"""Extract Qwen <tool_call>{...}</tool_call> blocks -> [{name, arguments}, ...].
Relies on the closing tag (not brace matching) so nested argument objects
parse correctly. Malformed JSON inside a block is skipped.
"""
calls = []
for body in _TOOL_CALL_RE.findall(text):
try:
calls.append(json.loads(body))
except json.JSONDecodeError:
continue
return calls