Files
param-hub-python/llm.py

207 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ParamHub LLM 智能解析模块
"""
import json
import base64
import requests
from config import CATEGORIES_FILE, IMAGES_DIR
from utils import load_data, get_llm_config
def get_parse_prompt_template(category_type, category_id=None, subcategory_id=None):
"""获取解析 prompt 模板(供前端显示和编辑)"""
categories = load_data(CATEGORIES_FILE)
if category_id:
cat = next((c for c in categories if c['id'] == category_id), None)
else:
type_to_cat_id = {'model': 'ai-models', 'gpu': 'gpus', 'cpu': 'cpus', 'dynamic': None}
cat_id = type_to_cat_id.get(category_type)
cat = next((c for c in categories if c['id'] == cat_id), None)
fields = _build_fields(cat, subcategory_id)
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
image_prompt = f"""请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{fields_json}
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. **提取数据时保留原始单位**:字段标签中如有单位标注(如($)、(GB)、(MHz)等),提取时请带上对应单位,保持数据完整性
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组 [对象列表]; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
return {
'fields': fields,
'fields_json': fields_json,
'image_prompt': image_prompt,
'category_name': cat.get('name', '') if cat else ''
}
def parse_with_llm(text, category_type, images=None, category_id=None,
subcategory_id=None, custom_prompt=None):
"""使用大模型解析文本/图片,提取结构化数据"""
categories = load_data(CATEGORIES_FILE)
if category_id:
cat = next((c for c in categories if c['id'] == category_id), None)
else:
type_to_cat_id = {'model': 'ai-models', 'gpu': 'gpus', 'cpu': 'cpus'}
cat_id = type_to_cat_id.get(category_type)
cat = next((c for c in categories if c['id'] == cat_id), None)
fields = _build_fields(cat, subcategory_id)
fields_json = json.dumps(fields, ensure_ascii=False, indent=2)
content_parts = []
if images and len(images) > 0:
if custom_prompt and custom_prompt.strip():
prompt_text = custom_prompt
else:
prompt_text = f"""请分析图片中的产品参数信息,提取结构化数据。
需要提取的字段:
{fields_json}
重要要求:
1. 图片中可能包含1个或多个产品请识别所有产品
2. 如果是多张图片,请综合分析所有图片内容
3. **提取数据时保留原始单位**:字段标签中如有单位标注,提取时请带上对应单位
4. 如果某字段没有提及返回null
5. 返回格式:如果识别到多个产品,返回数组; 如果只有一个产品,返回单个对象
6. 只返回JSON数据不要其他内容"""
content_parts.append({"type": "text", "text": prompt_text})
for img in images:
if isinstance(img, str):
if img.startswith('http'):
content_parts.append({"type": "image_url", "image_url": {"url": img}})
elif img.startswith('data:'):
content_parts.append({"type": "image_url", "image_url": {"url": img}})
else:
b64 = _load_local_image(img)
if b64:
content_parts.append({"type": "image_url", "image_url": {"url": b64}})
else:
prompt_text = f"""请解析以下文本,提取结构化数据。
文本内容:
{text}
需要提取的字段:
{fields_json}
要求:
1. 根据文本内容智能提取各个字段的值
2. **提取数据时保留原始单位**
3. 如果某字段在文本中没有提及返回null
4. 返回JSON格式不要包含任何其他内容
请直接返回JSON数据"""
content_parts.append({"type": "text", "text": prompt_text})
try:
llm_config = get_llm_config()
model = llm_config.get('vision_model', 'gpt-4-vision-preview') if images else llm_config['model']
response = requests.post(
f"{llm_config['base_url']}/chat/completions",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {llm_config['api_key']}"
},
json={
"model": model,
"messages": [
{"role": "system",
"content": "你是一个产品参数提取助手。只返回JSON不要其他内容。"},
{"role": "user", "content": content_parts}
],
"max_tokens": 2000,
"temperature": 0.1
},
timeout=60
)
if response.status_code == 200:
data = response.json()
content = data['choices'][0]['message']['content'].strip()
if content.startswith('```'):
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
content = content.rsplit('```', 1)[0] if '```' in content else content
parsed = json.loads(content)
results = parsed if isinstance(parsed, list) else [parsed]
return [_clean_result(item) for item in results]
except Exception as e:
print(f"LLM解析失败: {e}")
return [{'name': (text or '未命名产品')[:50], 'description': text}]
# ---- 内部函数 ----
def _build_fields(cat, subcategory_id):
if not cat or 'fields' not in cat:
return {
'name': '名称', 'brand': '品牌', 'price': '价格(数字)',
'year': '年份(数字)', 'specs': '规格参数(JSON对象)',
'description': '简介描述',
}
fields = {}
for field in cat['fields']:
desc = field['label']
desc += '(长文本)' if field.get('input_style') == 'long' else '(文本)'
if field.get('description'):
desc += f" - {field['description']}"
fields[field['key']] = desc
if subcategory_id:
subcat = next((s for s in cat.get('subcategories', []) if s['id'] == subcategory_id), None)
if subcat and 'extra_fields' in subcat:
for field in subcat['extra_fields']:
desc = field['label']
desc += '(长文本)' if field.get('input_style') == 'long' else '(文本)'
if field.get('description'):
desc += f" - {field['description']}"
fields[field['key']] = desc
return fields
def _load_local_image(img_src: str):
try:
img_path = IMAGES_DIR / img_src.replace('/static/uploads/', '')
if img_path.exists():
with open(img_path, 'rb') as f:
img_data = base64.b64encode(f.read()).decode()
ext = img_path.suffix.lower().lstrip('.')
mime = f'image/{"jpeg" if ext == "jpg" else ext}'
return f'data:{mime};base64,{img_data}'
except Exception:
pass
return None
def _clean_result(item: dict) -> dict:
cleaned = {}
for k, v in item.items():
if v is not None and v != '' and v != 'null':
if isinstance(v, str):
try:
cleaned[k] = float(v) if '.' in v else int(v)
except (ValueError, TypeError):
cleaned[k] = v
else:
cleaned[k] = v
return cleaned