hotfix(dc/tool-c): Fix compute formula validation and binning NaN serialization

Critical fixes:
1. Compute column: Add Chinese comma support in formula validation
   - Problem: Formula with Chinese comma failed validation
   - Fix: Add Chinese comma character to allowed_chars regex
   - Example: Support formulas like 'col1(kg)+ col2,col3'

2. Binning operation: Fix NaN serialization error
   - Problem: 'Out of range float values are not JSON compliant: nan'
   - Fix: Enhanced NaN/inf handling in binning endpoint
   - Added np.inf/-np.inf replacement before JSON serialization
   - Added manual JSON serialization with NaN->null conversion

3. Enhanced all operation endpoints for consistency
   - Updated conditional, dropna endpoints with same NaN/inf handling
   - Ensures all operations return JSON-compliant data

Modified files:
- extraction_service/operations/compute.py: Add Chinese comma to regex
- extraction_service/main.py: Enhanced NaN handling in binning/conditional/dropna

Status: Hotfix complete, ready for testing
This commit is contained in:
2025-12-09 08:45:27 +08:00
parent 91cab452d1
commit 75ceeb0653
79 changed files with 111 additions and 14 deletions

View File

@@ -228,3 +228,4 @@

View File

@@ -23,3 +23,4 @@ WHERE table_schema = 'dc_schema'

View File

@@ -8,3 +8,4 @@ ADD COLUMN IF NOT EXISTS "column_mapping" JSONB;
-- 添加注释
COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名映射:[{originalName, safeName, displayName}] 解决特殊字符问题';

View File

@@ -35,3 +35,4 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -185,3 +185,4 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -204,3 +204,4 @@ checkDCTables();

View File

@@ -156,3 +156,4 @@ createAiHistoryTable()

View File

@@ -143,3 +143,4 @@ createToolCTable()

View File

@@ -140,3 +140,4 @@ createToolCTable()

View File

@@ -308,3 +308,4 @@ runTests().catch((error) => {

View File

@@ -287,3 +287,4 @@ Content-Type: application/json

View File

@@ -366,3 +366,4 @@ export class ExcelExporter {

View File

@@ -223,3 +223,4 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -251,3 +251,4 @@ export const templateService = new TemplateService();

View File

@@ -173,3 +173,4 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -227,3 +227,4 @@ export const streamAIController = new StreamAIController();

View File

@@ -31,3 +31,4 @@ Write-Host "✅ 完成!" -ForegroundColor Green

View File

@@ -318,3 +318,4 @@ runAdvancedTests().catch(error => {

View File

@@ -384,3 +384,4 @@ runAllTests()

View File

@@ -342,3 +342,4 @@ runAllTests()

View File

@@ -1248,3 +1248,4 @@ interface FulltextScreeningResult {

View File

@@ -362,3 +362,4 @@ GET /api/v1/asl/fulltext-screening/tasks/:taskId/export

View File

@@ -464,3 +464,4 @@ Failed to open file '\\tmp\\extraction_service\\temp_10000_test.pdf'

View File

@@ -530,3 +530,4 @@ df['creatinine'] = pd.to_numeric(df['creatinine'], errors='coerce')

View File

@@ -368,3 +368,4 @@ npm run dev
**修复完成时间**: 2025-12-08 当前时间
**状态**: ✅ 已完成,待测试验证

View File

@@ -945,3 +945,4 @@ export const aiController = new AIController();

View File

@@ -1279,3 +1279,4 @@ npm install react-markdown

View File

@@ -602,3 +602,4 @@ import { logger } from '../../../../common/logging/index.js';

View File

@@ -405,3 +405,4 @@ import { ChatContainer } from '@/shared/components/Chat';

View File

@@ -315,3 +315,4 @@ const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{

View File

@@ -603,3 +603,4 @@ http://localhost:5173/data-cleaning/tool-c

View File

@@ -391,3 +391,4 @@ Docs: docs/03-业务模块/DC-数据清洗整理/06-开发记录/DC模块重建

View File

@@ -264,3 +264,4 @@ ConflictDetectionService // 冲突检测(字段级对比)

View File

@@ -428,3 +428,4 @@ Tool B后端代码**100%复用**了平台通用能力层,无任何重复开发

View File

@@ -205,3 +205,4 @@ $ node scripts/check-dc-tables.mjs

View File

@@ -438,3 +438,4 @@ ${fields.map((f, i) => `${i + 1}. ${f.name}${f.desc}`).join('\n')}

View File

@@ -452,3 +452,4 @@ import { ChatContainer } from '@/shared/components/Chat';

View File

@@ -851,15 +851,22 @@ async def operation_binning(request: BinningRequest):
request.num_bins
)
# 转换回JSON处理Categorical类型NaN值
# 转换回JSON处理Categorical类型NaN值和inf值
import numpy as np
# 1. 将Categorical列转为字符串
for col in result_df.columns:
if pd.api.types.is_categorical_dtype(result_df[col]):
result_df[col] = result_df[col].astype(str)
# 2. 将NaN替换为None避免JSON序列化错误
result_df_clean = result_df.fillna(value=pd.NA).replace({pd.NA: None})
result_data = result_df_clean.to_dict('records')
# 2. 替换inf和-inf为None
result_df = result_df.replace([np.inf, -np.inf], None)
# 3. 将NaN替换为None避免JSON序列化错误
result_df = result_df.fillna(value=pd.NA).replace({pd.NA: None})
# 4. 转换为dict
result_data = result_df.to_dict('records')
# 恢复stdout
sys.stdout = sys.__stdout__
@@ -869,13 +876,22 @@ async def operation_binning(request: BinningRequest):
logger.info(f"分箱成功: {request.column}{request.new_column_name}")
return JSONResponse(content={
# 使用json.dumps手动序列化确保NaN完全处理
import json
response_content = {
"success": True,
"result_data": result_data,
"output": output,
"execution_time": execution_time,
"result_shape": [len(result_data), len(result_df.columns)]
})
}
# 手动序列化NaN会被转为null
json_str = json.dumps(response_content, allow_nan=True)
# 替换NaN为null防止任何遗漏的NaN
json_str = json_str.replace('NaN', 'null').replace('Infinity', 'null').replace('-Infinity', 'null')
return JSONResponse(content=json.loads(json_str))
except Exception as e:
sys.stdout = sys.__stdout__
@@ -937,9 +953,11 @@ async def operation_conditional(request: ConditionalRequest):
request.else_value
)
# 转换回JSON处理NaN值
result_df_clean = result_df.fillna(value=pd.NA).replace({pd.NA: None})
result_data = result_df_clean.to_dict('records')
# 转换回JSON处理NaN和inf值)
import numpy as np
result_df = result_df.replace([np.inf, -np.inf], None)
result_df = result_df.fillna(value=pd.NA).replace({pd.NA: None})
result_data = result_df.to_dict('records')
# 恢复stdout
sys.stdout = sys.__stdout__
@@ -1015,9 +1033,11 @@ async def operation_dropna(request: DropnaRequest):
subset=request.subset
)
# 转换回JSON处理NaN值
result_df_clean = result_df.fillna(value=pd.NA).replace({pd.NA: None})
result_data = result_df_clean.to_dict('records')
# 转换回JSON处理NaN和inf值)
import numpy as np
result_df = result_df.replace([np.inf, -np.inf], None)
result_df = result_df.fillna(value=pd.NA).replace({pd.NA: None})
result_data = result_df.to_dict('records')
# 恢复stdout
sys.stdout = sys.__stdout__

View File

@@ -15,3 +15,4 @@
__version__ = '1.0.0'

View File

@@ -60,8 +60,8 @@ def validate_formula(formula: str, available_columns: list) -> tuple[bool, str]:
return False, f'公式包含不允许的操作: {pattern}'
# ✨ 增强:检查是否只包含允许的字符(放宽限制,支持更多特殊字符)
# 允许:英文字母、数字、下划线、中文、空格、运算符、括号(中英文)、逗号、点、冒号、等号
allowed_chars = r'[a-zA-Z0-9_\u4e00-\u9fa5\s\+\-\*/\(\)\[\]\{\}\.,:\*\*=()【】、。:;!?]'
# 允许:英文字母、数字、下划线、中文、空格、运算符、括号(中英文)、逗号(中英文)、点、冒号、等号
allowed_chars = r'[a-zA-Z0-9_\u4e00-\u9fa5\s\+\-\*/\(\)\[\]\{\}\.,:\*\*=()【】、。:;!?]'
if not re.match(f'^{allowed_chars}+$', formula):
# 找出不允许的字符
invalid_chars = set(re.findall(f'[^{allowed_chars}]', formula))

View File

@@ -148,3 +148,4 @@ def get_missing_summary(df: pd.DataFrame) -> dict:
}

View File

@@ -108,3 +108,4 @@ def apply_filter(
return result

View File

@@ -282,3 +282,4 @@ if __name__ == "__main__":

View File

@@ -48,3 +48,4 @@ except Exception as e:

View File

@@ -28,3 +28,4 @@ except Exception as e:

View File

@@ -517,3 +517,4 @@ export default FulltextDetailDrawer;

View File

@@ -116,3 +116,4 @@ export function useFulltextResults({

View File

@@ -79,3 +79,4 @@ export function useFulltextTask({

View File

@@ -470,3 +470,4 @@ export default FulltextResults;

View File

@@ -110,3 +110,4 @@ export const useAssets = (activeTab: AssetTabType) => {

View File

@@ -100,3 +100,4 @@ export const useRecentTasks = () => {

View File

@@ -336,3 +336,4 @@ const BinningDialog: React.FC<BinningDialogProps> = ({
export default BinningDialog;

View File

@@ -299,3 +299,4 @@ const DropnaDialog: React.FC<Props> = ({
export default DropnaDialog;

View File

@@ -261,3 +261,4 @@ const PivotDialog: React.FC<Props> = ({
export default PivotDialog;

View File

@@ -62,3 +62,4 @@ export interface DataStats {

View File

@@ -58,3 +58,4 @@ export type AssetTabType = 'all' | 'processed' | 'raw';

View File

@@ -13,3 +13,4 @@ export { default as Placeholder } from './Placeholder';

View File

@@ -15,3 +15,4 @@
__version__ = '1.0.0'

View File

@@ -122,3 +122,4 @@ def apply_binning(
return result

View File

@@ -108,3 +108,4 @@ def apply_filter(
return result

View File

@@ -78,3 +78,4 @@ def apply_recode(
return result

View File

@@ -222,3 +222,4 @@ if __name__ == "__main__":

View File

@@ -46,3 +46,4 @@ Write-Host "====================================================================