前后端基本架构和完全excel表的解析及统计图表的生成以及excel表的到出
This commit is contained in:
218
backend/app/services/text_analysis_service.py
Normal file
218
backend/app/services/text_analysis_service.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
文本分析服务 - 从 AI 分析结果中提取结构化数据用于可视化
|
||||
"""
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
import re
|
||||
import json
|
||||
|
||||
from app.services.llm_service import llm_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextAnalysisService:
|
||||
"""文本分析服务类"""
|
||||
|
||||
def __init__(self):
|
||||
self.llm_service = llm_service
|
||||
|
||||
async def extract_structured_data(
|
||||
self,
|
||||
analysis_text: str,
|
||||
original_filename: str = "",
|
||||
file_type: str = "text"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
从 AI 分析结果文本中提取结构化数据
|
||||
|
||||
Args:
|
||||
analysis_text: AI 分析结果文本
|
||||
original_filename: 原始文件名
|
||||
file_type: 文件类型
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 提取的结构化数据
|
||||
"""
|
||||
# 限制分析的文本长度,避免 token 超限
|
||||
max_text_length = 8000
|
||||
truncated_text = analysis_text[:max_text_length]
|
||||
|
||||
system_prompt = """你是一个专业的数据提取助手。你的任务是从AI分析结果中提取结构化数据,用于生成图表。
|
||||
|
||||
请按照以下要求提取数据:
|
||||
|
||||
1. 数值型数据:
|
||||
- 提取所有的数值、统计信息、百分比等
|
||||
- 为每个数值创建一个条目,包含:名称、值、单位(如果有)
|
||||
- 格式示例:{"name": "销售额", "value": 123456.78, "unit": "元"}
|
||||
|
||||
2. 分类数据:
|
||||
- 提取所有的类别、状态、枚举值等
|
||||
- 为每个类别创建一个条目,包含:名称、值、数量(如果有)
|
||||
- 格式示例:{"name": "产品类别", "value": "电子产品", "count": 25}
|
||||
|
||||
3. 时间序列数据:
|
||||
- 提取所有的时间相关数据(年月、季度、日期等)
|
||||
- 格式示例:{"name": "2025年1月", "value": 12345}
|
||||
|
||||
4. 对比数据:
|
||||
- 提取所有的对比、排名、趋势等数据
|
||||
- 格式示例:{"name": "同比增长", "value": 15.3, "unit": "%"}
|
||||
|
||||
5. 表格数据:
|
||||
- 如果分析结果中包含表格或列表形式的数据,提取出来
|
||||
- 格式:{"columns": ["列1", "列2"], "rows": [{"列1": "值1", "列2": "值2"}]}
|
||||
|
||||
重要规则:
|
||||
- 只提取明确提到的数据和数值
|
||||
- 如果某种类型的数据不存在,返回空数组 []
|
||||
- 确保所有数值都是有效的数字类型
|
||||
- 保持数据的原始精度
|
||||
- 返回的 JSON 必须完整且格式正确
|
||||
- 表格数据最多提取 20 行
|
||||
|
||||
请以 JSON 格式返回,不要添加任何 Markdown 标记或解释文字,只返回纯 JSON:
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"numeric_data": [
|
||||
{"name": string, "value": number, "unit": string|null}
|
||||
],
|
||||
"categorical_data": [
|
||||
{"name": string, "value": string, "count": number|null}
|
||||
],
|
||||
"time_series_data": [
|
||||
{"name": string, "value": number}
|
||||
],
|
||||
"comparison_data": [
|
||||
{"name": string, "value": number, "unit": string|null}
|
||||
],
|
||||
"table_data": {
|
||||
"columns": string[],
|
||||
"rows": object[]
|
||||
} | null
|
||||
},
|
||||
"metadata": {
|
||||
"total_items": number,
|
||||
"data_types": string[]
|
||||
}
|
||||
}"""
|
||||
|
||||
user_message = f"""请从以下 AI 分析结果中提取结构化数据:
|
||||
|
||||
原始文件名:{original_filename}
|
||||
文件类型:{file_type}
|
||||
|
||||
AI 分析结果:
|
||||
{truncated_text}
|
||||
|
||||
请按照系统提示的要求提取数据并返回纯 JSON 格式。"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_message}
|
||||
]
|
||||
|
||||
try:
|
||||
logger.info(f"开始提取结构化数据,文本长度: {len(truncated_text)}")
|
||||
|
||||
response = await self.llm_service.chat(
|
||||
messages=messages,
|
||||
temperature=0.1,
|
||||
max_tokens=4000
|
||||
)
|
||||
|
||||
content = self.llm_service.extract_message_content(response)
|
||||
logger.info(f"LLM 返回内容长度: {len(content)}")
|
||||
|
||||
# 使用简单的方法提取 JSON
|
||||
result = self._extract_json_simple(content)
|
||||
|
||||
if not result:
|
||||
logger.error("无法从 LLM 响应中提取有效的 JSON")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "AI 返回的数据格式不正确或被截断",
|
||||
"raw_content": content[:500]
|
||||
}
|
||||
|
||||
logger.info(f"成功提取结构化数据")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取结构化数据失败: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _extract_json_simple(self, content: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
简化的 JSON 提取方法
|
||||
|
||||
Args:
|
||||
content: LLM 返回的内容
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 解析后的 JSON,失败返回 None
|
||||
"""
|
||||
try:
|
||||
# 方法 1: 查找 ```json 代码块
|
||||
code_block_match = re.search(r'```json\n{[\s\S]*?}[\s\S]*?}\n```', content, re.DOTALL)
|
||||
if code_block_match:
|
||||
json_str = code_block_match.group(1)
|
||||
logger.info("从代码块中提取 JSON")
|
||||
return json.loads(json_str)
|
||||
|
||||
# 方法 2: 查找第一个完整的 { } 对象
|
||||
brace_count = 0
|
||||
json_start = -1
|
||||
|
||||
for i in range(len(content)):
|
||||
if content[i] == '{':
|
||||
if brace_count == 0:
|
||||
json_start = i
|
||||
brace_count += 1
|
||||
elif content[i] == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0:
|
||||
# 找到了完整的 JSON 对象
|
||||
json_end = i + 1
|
||||
json_str = content[json_start:json_end]
|
||||
logger.info(f"从大括号中提取 JSON")
|
||||
return json.loads(json_str)
|
||||
|
||||
# 方法 3: 尝试直接解析
|
||||
logger.info("尝试直接解析整个内容")
|
||||
return json.loads(content)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON 解析失败: {str(e)}")
|
||||
logger.error(f"原始内容(前 500 字符): {content[:500]}...")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"提取 JSON 失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def detect_data_types(self, data: Dict[str, Any]) -> List[str]:
|
||||
"""检测数据中包含的类型"""
|
||||
types = []
|
||||
d = data.get("data", {})
|
||||
|
||||
if d.get("numeric_data") and len(d["numeric_data"]) > 0:
|
||||
types.append("numeric")
|
||||
if d.get("categorical_data") and len(d["categorical_data"]) > 0:
|
||||
types.append("categorical")
|
||||
if d.get("time_series_data") and len(d["time_series_data"]) > 0:
|
||||
types.append("time_series")
|
||||
if d.get("comparison_data") and len(d["comparison_data"]) > 0:
|
||||
types.append("comparison")
|
||||
if d.get("table_data") and d["table_data"]:
|
||||
types.append("table")
|
||||
|
||||
return types
|
||||
|
||||
|
||||
# 全局单例
|
||||
text_analysis_service = TextAnalysisService()
|
||||
Reference in New Issue
Block a user