This commit is contained in:
vikneshbala2021 2026-06-18 16:56:53 +08:00 committed by GitHub
commit c2c98ff3fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 254 additions and 42 deletions

View File

@ -527,16 +527,39 @@ class OasisProfileGenerator:
for attempt in range(max_attempts): for attempt in range(max_attempts):
try: try:
response = self.client.chat.completions.create( kwargs = {
model=self.model_name, "model": self.model_name,
messages=[ "messages": [
{"role": "system", "content": self._get_system_prompt(is_individual)}, {"role": "system", "content": self._get_system_prompt(is_individual)},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
], ],
response_format={"type": "json_object"}, "response_format": {"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 # 部分模型不支持自定义温度,后续会自动降级重试
# 不设置max_tokens,让LLM自由发挥 "temperature": 0.7 - (attempt * 0.1)
) }
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
continue
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content content = response.choices[0].message.content

View File

@ -440,16 +440,39 @@ class SimulationConfigGenerator:
for attempt in range(max_attempts): for attempt in range(max_attempts):
try: try:
response = self.client.chat.completions.create( kwargs = {
model=self.model_name, "model": self.model_name,
messages=[ "messages": [
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
{"role": "user", "content": prompt} {"role": "user", "content": prompt}
], ],
response_format={"type": "json_object"}, "response_format": {"type": "json_object"},
temperature=0.7 - (attempt * 0.1) # 每次重试降低温度 # 部分模型不支持自定义温度,后续会自动降级重试
# 不设置max_tokens,让LLM自由发挥 "temperature": 0.7 - (attempt * 0.1)
) }
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
continue
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content content = response.choices[0].message.content
finish_reason = response.choices[0].finish_reason finish_reason = response.choices[0].finish_reason

View File

@ -1,18 +1,47 @@
""" """
LLM客户端封装 LLM客户端封装
统一使用OpenAI格式调用 支持OpenAI和Anthropic格式的API调用
""" """
import json import json
import re import re
from urllib.parse import urlparse
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from openai import OpenAI from openai import OpenAI
from ..config import Config from ..config import Config
# 尝试导入Anthropic SDK,如果不可用则设为None
try:
from anthropic import Anthropic
except ImportError:
Anthropic = None
try:
from anthropic import AnthropicFoundry
except ImportError:
AnthropicFoundry = None
def _foundry_resource_from_endpoint(endpoint: str) -> str:
"""从 Foundry endpoint 提取 resource 名称。"""
if not endpoint:
raise ValueError("LLM_BASE_URL is empty")
if "://" not in endpoint:
return endpoint.strip()
host = urlparse(endpoint).netloc
m = re.match(r"([^.]+)\.services\.ai\.azure\.com$", host)
if not m:
raise ValueError(
f"Could not parse Foundry resource from endpoint: {endpoint!r}. "
"Expected https://<resource>.services.ai.azure.com/anthropic/"
)
return m.group(1)
class LLMClient: class LLMClient:
"""LLM客户端""" """LLM客户端 - 支持OpenAI和Anthropic格式"""
def __init__( def __init__(
self, self,
@ -27,10 +56,60 @@ class LLMClient:
if not self.api_key: if not self.api_key:
raise ValueError("LLM_API_KEY 未配置") raise ValueError("LLM_API_KEY 未配置")
self.client = OpenAI( # 检测是否是Anthropic格式的端点
api_key=self.api_key, self.is_anthropic = "anthropic" in self.base_url.lower()
base_url=self.base_url
) if self.is_anthropic:
# 使用Anthropic SDK
if Anthropic is None:
raise ImportError("Anthropic SDK未安装。请运行: pip install anthropic")
is_foundry_endpoint = "services.ai.azure.com" in self.base_url.lower()
if is_foundry_endpoint and AnthropicFoundry is not None:
resource = _foundry_resource_from_endpoint(self.base_url)
self.client = AnthropicFoundry(
api_key=self.api_key,
resource=resource,
)
else:
self.client = Anthropic(
api_key=self.api_key,
base_url=self.base_url
)
self.client_type = "anthropic"
else:
# 使用OpenAI SDK
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
self.client_type = "openai"
@staticmethod
def _convert_messages_for_anthropic(messages: List[Dict[str, str]]) -> Dict[str, Any]:
"""将 OpenAI 风格消息转换为 Anthropic Messages API 所需格式。"""
system_parts: List[str] = []
anthropic_messages: List[Dict[str, str]] = []
for msg in messages:
role = msg.get("role", "")
content = msg.get("content", "")
if role == "system":
if content:
system_parts.append(content)
continue
if role not in {"user", "assistant"}:
role = "user"
anthropic_messages.append({"role": role, "content": content})
if not anthropic_messages:
anthropic_messages = [{"role": "user", "content": ""}]
result: Dict[str, Any] = {"messages": anthropic_messages}
if system_parts:
result["system"] = "\n\n".join(system_parts)
return result
def chat( def chat(
self, self,
@ -51,18 +130,65 @@ class LLMClient:
Returns: Returns:
模型响应文本 模型响应文本
""" """
kwargs = { if self.client_type == "anthropic":
"model": self.model, # 使用Anthropic SDK(system 需单独传递,messages 仅支持 user/assistant)
"messages": messages, converted = self._convert_messages_for_anthropic(messages)
"temperature": temperature, create_kwargs = {
"max_tokens": max_tokens, "model": self.model,
} "messages": converted["messages"],
"system": converted.get("system"),
"max_tokens": max_tokens,
"temperature": temperature,
}
try:
response = self.client.messages.create(**create_kwargs)
except Exception as e:
# 某些新模型会拒绝 temperature 参数(报错: temperature is deprecated)
if "temperature" in str(e).lower() and "deprecated" in str(e).lower():
create_kwargs.pop("temperature", None)
response = self.client.messages.create(**create_kwargs)
else:
raise
content = response.content[0].text
else:
# 使用OpenAI SDK
kwargs = {
"model": self.model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
if response_format:
kwargs["response_format"] = response_format
response = None
for _ in range(3):
try:
response = self.client.chat.completions.create(**kwargs)
break
except Exception as e:
err_text = str(e).lower()
# 某些模型(如部分 GPT-5 系列)不支持 max_tokens,仅支持 max_completion_tokens
if "unsupported parameter" in err_text and "max_tokens" in err_text and "max_tokens" in kwargs:
kwargs.pop("max_tokens", None)
kwargs["max_completion_tokens"] = max_tokens
continue
# 某些模型仅支持默认 temperature(通常是 1)
if "temperature" in err_text and (
"unsupported value" in err_text or "default (1) value" in err_text
) and "temperature" in kwargs:
kwargs.pop("temperature", None)
continue
raise
if response is None:
raise RuntimeError("OpenAI chat completion failed after compatibility retries")
content = response.choices[0].message.content
if response_format:
kwargs["response_format"] = response_format
response = self.client.chat.completions.create(**kwargs)
content = response.choices[0].message.content
# 部分模型(如MiniMax M2.5)会在content中包含<think>思考内容,需要移除 # 部分模型(如MiniMax M2.5)会在content中包含<think>思考内容,需要移除
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip() content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
return content return content
@ -84,14 +210,34 @@ class LLMClient:
Returns: Returns:
解析后的JSON对象 解析后的JSON对象
""" """
response = self.chat( if self.client_type == "anthropic":
messages=messages, modified_messages = [dict(m) for m in messages]
temperature=temperature, json_instruction = "IMPORTANT: You must respond with valid JSON only, no other text."
max_tokens=max_tokens, system_index = next((i for i, m in enumerate(modified_messages) if m.get("role") == "system"), None)
response_format={"type": "json_object"} if system_index is None:
) modified_messages.insert(0, {"role": "system", "content": json_instruction})
else:
modified_messages[system_index]["content"] = (
f"{modified_messages[system_index].get('content', '')}\n\n{json_instruction}"
)
response_text = self.chat(
messages=modified_messages,
temperature=temperature,
max_tokens=max_tokens,
)
else:
# OpenAI格式:使用response_format
response = self.chat(
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format={"type": "json_object"}
)
response_text = response
# 清理markdown代码块标记 # 清理markdown代码块标记
cleaned_response = response.strip() cleaned_response = response_text.strip()
cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE)
cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response) cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response)
cleaned_response = cleaned_response.strip() cleaned_response = cleaned_response.strip()

View File

@ -15,6 +15,7 @@ dependencies = [
# LLM 相关 # LLM 相关
"openai>=1.0.0", "openai>=1.0.0",
"anthropic>=0.71.0",
# Zep Cloud # Zep Cloud
"zep-cloud==3.13.0", "zep-cloud==3.13.0",

View File

@ -12,6 +12,8 @@ flask-cors>=6.0.0
# ============= LLM 相关 ============= # ============= LLM 相关 =============
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM) # OpenAI SDK(统一使用 OpenAI 格式调用 LLM)
openai>=1.0.0 openai>=1.0.0
# Anthropic SDK(支持 Azure Foundry Anthropic 端点)
anthropic>=0.71.0
# ============= Zep Cloud ============= # ============= Zep Cloud =============
zep-cloud==3.13.0 zep-cloud==3.13.0

View File

@ -24,6 +24,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
] ]
[[package]]
name = "anthropic"
version = "0.109.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "docstring-parser" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/b7/9a8e2f79011e89dd6eeb599c27332aed765dac9d6fbee3a55e68e4e3ec25/anthropic-0.109.2.tar.gz", hash = "sha256:d37db299597c7bc124b49b767ff135f1e6456b64af2b2fad4b63b2a1df333cf0", size = 927559, upload-time = "2026-06-15T17:30:25.024Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/f2/bee5de8a2699fc8a3cce34d61c7a2626a2c310ddde7ea5611327eb0ddbe9/anthropic-0.109.2-py3-none-any.whl", hash = "sha256:e0fb4ca5df0ed983248c9c6c3242adc81d9cfddb8725902da53698554117abac", size = 923800, upload-time = "2026-06-15T17:30:23.124Z" },
]
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.12.0" version = "4.12.0"
@ -988,6 +1007,7 @@ name = "mirofish-backend"
version = "0.1.0" version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "anthropic" },
{ name = "camel-ai" }, { name = "camel-ai" },
{ name = "camel-oasis" }, { name = "camel-oasis" },
{ name = "chardet" }, { name = "chardet" },
@ -1016,6 +1036,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "anthropic", specifier = ">=0.71.0" },
{ name = "camel-ai", specifier = "==0.2.78" }, { name = "camel-ai", specifier = "==0.2.78" },
{ name = "camel-oasis", specifier = "==0.2.5" }, { name = "camel-oasis", specifier = "==0.2.5" },
{ name = "chardet", specifier = ">=5.0.0" }, { name = "chardet", specifier = ">=5.0.0" },

View File

@ -1435,7 +1435,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
@ -1913,7 +1912,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -2053,7 +2051,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -2128,7 +2125,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.25", "@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25", "@vue/compiler-sfc": "3.5.25",