当前位置: 首页 > news >正文

llm

import logging
import json
import difflib
import re
import os
import requests
import pytesseract

from PIL import Image, ImageOps
from io import BytesIO
from typing import Union, List, Dict, Optional, Any, Tuple
from tenacity import retry, stop_after_attempt, wait_random
from openai import OpenAI # 只需要 OpenAI 客户端

from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.response import ModelResponse
from label_studio_sdk.label_interface.objects import PredictionValue
from label_studio_sdk.label_interface.object_tags import ImageTag, ParagraphsTag
from label_studio_sdk.label_interface.control_tags import ControlTag, ObjectTag

logger = logging.getLogger(__name__)

# =======================
# SiliconFlow 固定配置
# =======================
SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"
SILICONFLOW_API_KEY = "sk-deezdpffwyyhwglpqbyfyskukllsqjwbhutfgjwjwxnjdzvf" # TODO: 换成你的真实密钥
SILICONFLOW_MODEL = "Qwen/Qwen3-8B"

# 固定的系统提示词(角色设定)
SYSTEM_PROMPT = "你是一个擅长中文文本摘要的助手。请在保留关键信息的前提下用简洁的中文进行概括。"

# 固定的摘要任务指令
TASK_INSTRUCTION = "请对下面的文本进行摘要,突出主要内容,长度控制在200字以内。"


@retry(wait=wait_random(min=5, max=10), stop=stop_after_attempt(6))
def chat_completion_call(messages, params, *args, **kwargs):
"""
使用 openai SDK 以 SiliconFlow 为后端,调用 chat completions
"""
# 忽略 params 里的 provider / api_key / model,统一使用写死的 SiliconFlow 配置
client = OpenAI(
base_url=SILICONFLOW_BASE_URL,
api_key=SILICONFLOW_API_KEY,
)

temperature = params.get("temperature", OpenAIInteractive.TEMPERATURE)
n = params.get("num_responses", OpenAIInteractive.NUM_RESPONSES)

request_params = {
"messages": messages,
"model": SILICONFLOW_MODEL,
"n": n,
"temperature": temperature,
"max_tokens": 1000, # 如有需要可调整
}

logger.info(f"SiliconFlow(OpenAI SDK) request_params: {request_params}")
completion = client.chat.completions.create(**request_params)
logger.info(f"SiliconFlow(OpenAI SDK) completion: {completion}")

return completion


def gpt(messages: Union[List[Dict], str], params, *args, **kwargs):
"""
使用固定的系统提示 + 摘要任务指令,调用 SiliconFlow Qwen 模型,返回 List[str]
"""
full_messages: List[Dict[str, str]] = []

# System 角色:设定为中文摘要助手
full_messages.append({"role": "system", "content": SYSTEM_PROMPT})

# 如果传入是字符串,把它当成“要摘要的原文”
if isinstance(messages, str):
user_content = f"{TASK_INSTRUCTION}\n\n原文如下:\n{messages}"
full_messages.append({"role": "user", "content": user_content})
else:
# 兼容原来的调用方式:messages 为 list[dict]
# 在第一个 user 消息前插入 TASK_INSTRUCTION
inserted = False
for m in messages:
if m.get("role") == "user" and not inserted:
new_content = f"{TASK_INSTRUCTION}\n\n原文如下:\n{m.get('content', '')}"
full_messages.append({"role": "user", "content": new_content})
inserted = True
else:
full_messages.append(m)
if not inserted:
# 如果没有 user 消息则补一个
full_messages.append({"role": "user", "content": TASK_INSTRUCTION})

logger.info(f"SiliconFlow(OpenAI SDK) request messages: {full_messages}, params={params}")
completion = chat_completion_call(full_messages, params)
logger.info(f"SiliconFlow(OpenAI SDK) response: {completion}")

# completion.choices[i].message.content
response = [choice.message.content for choice in completion.choices]
return response


class OpenAIInteractive(LabelStudioMLBase):
"""
基于 SiliconFlow Qwen 模型的 Text Summarization ML backend
使用 Label Studio 的 OpenAIInteractive 模板,底层改为 SiliconFlow
"""

# 这些环境变量可以保留(部分仍被使用)
OPENAI_PROVIDER = os.getenv("OPENAI_PROVIDER", "openai")
OPENAI_KEY = os.getenv('OPENAI_API_KEY')
PROMPT_PREFIX = os.getenv("PROMPT_PREFIX", "prompt")
USE_INTERNAL_PROMPT_TEMPLATE = bool(int(os.getenv("USE_INTERNAL_PROMPT_TEMPLATE", 1)))
# 可选:会被 setup() 当作文件路径读取;但摘要逻辑主要由 SYSTEM_PROMPT/TASK_INSTRUCTION 控制
DEFAULT_PROMPT = os.getenv('DEFAULT_PROMPT')
PROMPT_TEMPLATE = os.getenv(
"PROMPT_TEMPLATE",
'**Source Text**:\n\n"{text}"\n\n**Task Directive**:\n\n"{prompt}"'
)
PROMPT_TAG = "TextArea"
SUPPORTED_INPUTS = ("Image", "Text", "HyperText", "Paragraphs")
NUM_RESPONSES = int(os.getenv("NUM_RESPONSES", 1))
TEMPERATURE = float(os.getenv("TEMPERATURE", 0.7))
OPENAI_MODEL = os.getenv("OPENAI_MODEL")
AZURE_RESOURCE_ENDPOINT = os.getenv("AZURE_RESOURCE_ENDPOINT", '')
AZURE_DEPLOYMENT_NAME = os.getenv("AZURE_DEPLOYMENT_NAME")
AZURE_API_VERSION = os.getenv("AZURE_API_VERSION", "2023-05-15")
OLLAMA_ENDPOINT = os.getenv("OLLAMA_ENDPOINT")

def setup(self):
if self.DEFAULT_PROMPT and os.path.isfile(self.DEFAULT_PROMPT):
logger.info(f"Reading default prompt from file: {self.DEFAULT_PROMPT}")
with open(self.DEFAULT_PROMPT, encoding="utf-8") as f:
self.DEFAULT_PROMPT = f.read()

def _ocr(self, image_url):
# Open the image containing the text
response = requests.get(image_url)
image = Image.open(BytesIO(response.content))
image = ImageOps.exif_transpose(image)

# Run OCR on the image
text = pytesseract.image_to_string(image)
return text

def _get_text(self, task_data, object_tag):
"""
根据 object_tag 类型,从 task_data 中提取文本:
- Image: OCR
- Paragraphs: JSON 序列化
- Text/HyperText: 原样返回
"""
data = task_data.get(object_tag.value_name)

if data is None:
return None

if isinstance(object_tag, ImageTag):
return self._ocr(data)
elif isinstance(object_tag, ParagraphsTag):
return json.dumps(data, ensure_ascii=False)
else:
return data

def _get_prompts(self, context, prompt_tag) -> List[str]:
"""获取 Prompt 的值(交互模式 / 存储 / 默认 prompt)"""
if context:
# 交互模式 - 从 context 里读当前 prompt
result = context.get('result')
if result:
for item in result:
if item.get('from_name') == prompt_tag.name:
return item['value']['text']
# 初始化 - 从内部存储读取
elif prompt := self.get(prompt_tag.name):
return [prompt]
# 默认 prompt(注意,如果 USE_INTERNAL_PROMPT_TEMPLATE=1 时会报错并忽略)
elif self.DEFAULT_PROMPT:
if self.USE_INTERNAL_PROMPT_TEMPLATE:
logger.error(
'Using both `DEFAULT_PROMPT` and `USE_INTERNAL_PROMPT_TEMPLATE` is not supported. '
'Please either specify `USE_INTERNAL_PROMPT_TEMPLATE=0` or remove `DEFAULT_PROMPT`. '
'For now, no prompt will be used.'
)
return []
return [self.DEFAULT_PROMPT]

return []

def _match_choices(self, response: List[str], original_choices: List[str]) -> List[str]:
# assuming classes are separated by newlines
matched_labels = []
predicted_classes = response[0].splitlines()

for pred in predicted_classes:
scores = list(
map(lambda l: difflib.SequenceMatcher(None, pred, l).ratio(), original_choices)
)
matched_labels.append(original_choices[scores.index(max(scores))])

return matched_labels

def _find_choices_tag(self, object_tag):
"""Classification predictor"""
li = self.label_interface

try:
choices_from_name, _, _ = li.get_first_tag_occurence(
'Choices',
self.SUPPORTED_INPUTS,
to_name_filter=lambda s: s == object_tag.name,
)

return li.get_control(choices_from_name)
except Exception:
return None

def _find_textarea_tag(self, prompt_tag, object_tag):
"""Free-form text predictor"""
li = self.label_interface

try:
textarea_from_name, _, _ = li.get_first_tag_occurence(
'TextArea',
self.SUPPORTED_INPUTS,
name_filter=lambda s: s != prompt_tag.name,
to_name_filter=lambda s: s == object_tag.name,
)

return li.get_control(textarea_from_name)
except Exception:
return None

def _find_prompt_tags(self) -> Tuple[ControlTag, ObjectTag]:
"""在配置中找到 Prompt 用的 TextArea 以及对应的对象标签"""
li = self.label_interface
prompt_from_name, prompt_to_name, value = li.get_first_tag_occurence(
# prompt tag
self.PROMPT_TAG,
# supported input types
self.SUPPORTED_INPUTS,
# 如果有多个 <TextArea>,选择 name 以 PROMPT_PREFIX 开头的
name_filter=lambda s: s.startswith(self.PROMPT_PREFIX),
)

return li.get_control(prompt_from_name), li.get_object(prompt_to_name)

def _validate_tags(self, choices_tag: str, textarea_tag: str) -> None:
if not choices_tag and not textarea_tag:
raise ValueError('No supported tags found: <Choices> or <TextArea>')

def _generate_normalized_prompt(
self, text: str, prompt: str, task_data: Dict, labels: Optional[List[str]]
) -> str:
"""
因为我们已经在 gpt() 里写死了摘要任务提示,这里的 norm_prompt 可以简单理解为“原文 text”,
但为了兼容原逻辑,仍保留两种模式:
- USE_INTERNAL_PROMPT_TEMPLATE=1: 用 PROMPT_TEMPLATE(text, prompt, labels)
- 否则: 把 prompt 当作 format 模板
"""
if self.USE_INTERNAL_PROMPT_TEMPLATE:
norm_prompt = self.PROMPT_TEMPLATE.format(text=text, prompt=prompt, labels=labels)
else:
# 注意:这里传入的 task_data 中需要包含 text 对应字段,或你自定义的字段
norm_prompt = prompt.format(labels=labels, **task_data)

return norm_prompt

def _generate_response_regions(
self,
response: List[str],
prompt_tag,
choices_tag: ControlTag,
textarea_tag: ControlTag,
prompts: List[str],
) -> List:
"""
把 LLM 返回结果映射为 Label Studio 的 regions
"""
regions = []

if choices_tag and len(response) > 0:
matched_labels = self._match_choices(response, choices_tag.labels)
regions.append(choices_tag.label(matched_labels))

if textarea_tag:
# 对于 Text Summarization,通常我们期望把摘要填入一个 TextArea(比如 name="summary")
regions.append(textarea_tag.label(text=response))

# 把当前使用的 prompt 也记录下来
regions.append(prompt_tag.label(text=prompts))

return regions

def _predict_single_task(
self,
task_data: Dict,
prompt_tag: Any,
object_tag: Any,
prompt: str,
choices_tag: ControlTag,
textarea_tag: ControlTag,
prompts: List[str],
) -> Dict:
"""
对单个任务调用 Qwen 做摘要,并构造 PredictionValue
"""
text = self._get_text(task_data, object_tag)
# 如果有 Choices,则把 labels 传给 prompt(此处主要用于分类任务,摘要时一般不用)
labels = choices_tag.labels if choices_tag else None
norm_prompt = self._generate_normalized_prompt(text, prompt, task_data, labels=labels)

# run inference(底层已改为 SiliconFlow Qwen)
response = gpt(norm_prompt, self.extra_params)
regions = self._generate_response_regions(response, prompt_tag, choices_tag, textarea_tag, prompts)

return PredictionValue(result=regions, score=0.1, model_version=str(self.model_version))

def predict(self, tasks: List[Dict], context: Optional[Dict] = None, **kwargs) -> ModelResponse:
"""
Label Studio 调用的预测入口
"""
predictions = []

# prompt_tag: Prompt 的 TextArea
# object_tag: 我们要做摘要/标注的输入对象
prompt_tag, object_tag = self._find_prompt_tags()
prompts = self._get_prompts(context, prompt_tag)

if prompts:
prompt = "\n".join(prompts)

choices_tag = self._find_choices_tag(object_tag)
textarea_tag = self._find_textarea_tag(prompt_tag, object_tag)
self._validate_tags(choices_tag, textarea_tag)

for task in tasks:
# preload all task data fields, they are needed for prompt
task_data = self.preload_task_data(task, task['data'])
pred = self._predict_single_task(
task_data, prompt_tag, object_tag, prompt, choices_tag, textarea_tag, prompts
)
predictions.append(pred)

return ModelResponse(predictions=predictions)

def _prompt_diff(self, old_prompt, new_prompt):
"""
比较旧 prompt 和新 prompt 的差异
"""
old_lines = old_prompt.splitlines()
new_lines = new_prompt.splitlines()
diff = difflib.unified_diff(old_lines, new_lines, lineterm="")

return "\n".join(
line for line in diff if line.startswith(('+',)) and not line.startswith(('+++', '---'))
)

def fit(self, event, data, **additional_params):
"""
训练接口:这里只用来记录 Prompt 和更新 model_version(Prompt Tuning)
"""
logger.info(f'Data received: {data}')
if event not in ('ANNOTATION_CREATED', 'ANNOTATION_UPDATED'):
return

prompt_tag, object_tag = self._find_prompt_tags()
prompts = self._get_prompts(data['annotation'], prompt_tag)

if not prompts:
logger.info('No prompts recorded.')
return

prompt = '\n'.join(prompts)
current_prompt = self.get(prompt_tag.name)

# 如果没有 Prompt 差异,就不更新版本
if current_prompt:
diff = self._prompt_diff(current_prompt, prompt)
if not diff:
logger.info('No prompt diff found.')
return

logger.info(f'Prompt diff: {diff}')

self.set(prompt_tag.name, prompt)
model_version = self.bump_model_version()
logger.info(f'Updated model version to {str(model_version)}')
http://www.jsqmd.com/news/268253/

相关文章:

  • 语义搜索入门利器:集成可视化界面的GTE相似度计算工具
  • 5分钟掌握QtUsb:跨平台USB开发的终极解决方案
  • 为什么IQuest-Coder-V1需要专用GPU?算力需求深度解析
  • Python Web 开发进阶实战:时空数据引擎 —— 在 Flask + Vue 中构建实时地理围栏与轨迹分析系统
  • 闲置京东e卡兑换,让沉睡资源重焕生机! - 京顺回收
  • FunASR语音识别实战:教育领域口语评测系统搭建
  • 2026真空干燥机厂家推荐:江苏永佳干燥科技,立式/四轴/空心/卧式等全系真空干燥设备供应
  • Python Web 开发进阶实战:可验证网络 —— 在 Flask + Vue 中实现去中心化身份(DID)与零知识证明(ZKP)认证
  • ROFL-Player英雄联盟回放分析工具终极使用指南
  • 杭州婚纱摄影推荐综合评分排名;几大品牌打造出圈杭州婚纱照 - charlieruizvin
  • 5分钟快速上手GitHub Actions运行器镜像:终极开发环境搭建指南
  • Nextcloud AIO部署终极指南:从零搭建全栈环境
  • 如何快速掌握IDM-VTON:虚拟试衣模型的完整教程
  • 腾讯混元MT模型应用场景:中小企业本地化部署指南
  • AirSim无人机仿真平台:完整部署指南与实战技巧
  • Navicat x 达梦技术指引 | 数据生成
  • 2026MBTI测试平台最新推荐,MBTI测试官网,MBTI免费测试,MBTI官方测试,MBTI在线测试,MBTI测试,中文MBTI测试平台选择指南! - 品牌鉴赏师
  • Nucleus Co-Op:单机游戏变身多人同乐的终极解决方案
  • 实测Sambert多情感语音合成:中文配音效果惊艳实录
  • 2026年济南美术高考培训指南:道北画室,1400+学员高分实证的济南画室首选 - 深度智识库
  • 电脑定时助手,支持定时关机等多种任务,一键设置搞定!使用完全免费~
  • 效果惊艳!AutoGen Studio+Qwen3-4B生成的AI绘画案例展示
  • 制造业专属工具崛起:通用平台正在失效?
  • 汽车软件越来越复杂,测试这件事,真的不能再“靠人扛”了!
  • 实测通义千问3-4B:手机跑大模型的真实体验分享
  • 1701RZ14003D控制器
  • 如何验证UDP传输是否已经溢出?
  • 文件名怎么批量修改?这款工具可一键批量对文件重命名,使用完全免费,有多种命名方法!
  • 图片格式转换神器,可同时对图片进行压缩,非常强大!
  • 文献怎么查:高效查找文献的实用方法与步骤指南