ppt_agent / server /llm_client.py
Berlm's picture
Upload folder using huggingface_hub
ba2cf79 verified
Raw History Blame Contribute Delete
7.3 kB
"""Unified OpenAI-client wrapper for environment-side judge LLM calls."""
import json
import logging
import os
import re
from openai import OpenAI
from .debug_logging import write_debug_event
logger = logging.getLogger(__name__)
def _exception_payload(error: Exception) -> dict[str, object]:
payload: dict[str, object] = {
"error": str(error),
"error_type": error.__class__.__name__,
"error_repr": repr(error),
}
if getattr(error, "args", None):
payload["error_args"] = [str(arg) for arg in error.args]
cause = error.__cause__
if cause is not None:
payload["cause_type"] = cause.__class__.__name__
payload["cause"] = str(cause)
payload["cause_repr"] = repr(cause)
context = error.__context__
if context is not None and context is not cause:
payload["context_type"] = context.__class__.__name__
payload["context"] = str(context)
payload["context_repr"] = repr(context)
return payload
class LLMClient:
"""Thin wrapper around the OpenAI client using judge env vars."""
def __init__(self):
self.model = os.environ.get(
"JUDGE_MODEL_NAME",
os.environ.get(
"MODEL_NAME",
),
)
self.base_url = os.environ.get(
"JUDGE_API_BASE_URL",
os.environ.get(
"API_BASE_URL",
"https://router.huggingface.co/v1",
),
)
api_key = os.environ.get("JUDGE_API_KEY", os.environ.get("HF_TOKEN"))
self.client = OpenAI(base_url=self.base_url, api_key=api_key)
logger.info(
"Judge LLM client configured model=%s base_url=%s",
self.model,
self.base_url,
)
def chat(
self,
system: str,
user: str,
temperature: float = 0.3,
max_tokens: int = 1024,
*,
debug_stage: str | None = None,
) -> str:
"""Send a chat completion request. Returns the raw response text."""
return self._chat_openai(
system,
user,
temperature,
max_tokens,
debug_stage=debug_stage,
)
def chat_json(
self,
system: str,
user: str,
temperature: float = 0.3,
max_tokens: int = 1024,
*,
debug_stage: str | None = None,
) -> dict:
"""Send a chat request and parse the response as JSON."""
raw = self._chat_openai(
system,
user,
temperature,
max_tokens,
json_mode=True,
debug_stage=debug_stage,
)
return self._parse_json(raw)
@staticmethod
def _parse_json(raw: str) -> dict:
"""Extract and parse JSON from LLM response, handling markdown fences."""
if raw is None:
raise ValueError("LLM returned empty content")
raw = str(raw).strip()
raw = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
# Strip ```json ... ``` or ``` ... ``` wrappers
fence_match = re.search(r"```(?:json)?\s*\n?(.*?)```", raw, re.DOTALL)
if fence_match:
raw = fence_match.group(1).strip()
if not raw:
raise ValueError("LLM returned empty content")
try:
return json.loads(raw)
except json.JSONDecodeError:
object_match = re.search(r"\{.*\}", raw, re.DOTALL)
if object_match is None:
raise
return json.loads(object_match.group(0))
@staticmethod
def _extract_chat_content(message_content) -> str:
if isinstance(message_content, str):
return message_content
if message_content is None:
return ""
if isinstance(message_content, list):
parts: list[str] = []
for item in message_content:
if isinstance(item, str):
parts.append(item)
continue
if isinstance(item, dict):
text_value = item.get("text")
if isinstance(text_value, str):
parts.append(text_value)
elif item.get("type") == "text" and isinstance(
item.get("content"), str
):
parts.append(item["content"])
continue
text_attr = getattr(item, "text", None)
if isinstance(text_attr, str):
parts.append(text_attr)
return "\n".join(part for part in parts if part)
return str(message_content)
def _chat_openai(
self,
system: str,
user: str,
temperature: float,
max_tokens: int,
*,
json_mode: bool = False,
debug_stage: str | None = None,
) -> str:
request_kwargs = {
"model": self.model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": user},
],
"temperature": temperature,
"max_tokens": max_tokens,
}
if json_mode:
request_kwargs["response_format"] = {"type": "json_object"}
stage = debug_stage or "chat"
write_debug_event(
"llm.request",
{
"stage": stage,
"model": self.model,
"base_url": self.base_url,
"json_mode": json_mode,
"temperature": temperature,
"max_tokens": max_tokens,
"system": system,
"user": user,
},
)
try:
response = self.client.chat.completions.create(**request_kwargs)
except Exception as error:
if not json_mode:
write_debug_event(
"llm.error",
{
"stage": stage,
"model": self.model,
"base_url": self.base_url,
"json_mode": json_mode,
**_exception_payload(error),
},
)
raise
request_kwargs.pop("response_format", None)
try:
response = self.client.chat.completions.create(**request_kwargs)
except Exception as retry_error:
write_debug_event(
"llm.error",
{
"stage": stage,
"model": self.model,
"base_url": self.base_url,
"json_mode": json_mode,
**_exception_payload(retry_error),
"response_format_retry": True,
},
)
raise
content = self._extract_chat_content(response.choices[0].message.content)
write_debug_event(
"llm.response",
{
"stage": stage,
"model": self.model,
"json_mode": json_mode,
"content": content,
},
)
return content