This commit is contained in:
gongwenxin
2025-06-01 00:38:36 +08:00
parent a4e175bc15
commit b3ac2408be
13 changed files with 5048 additions and 6740 deletions
+61 -3
View File
@@ -5,7 +5,6 @@ import json
import logging
import re
from typing import Optional, Dict, Any, List
import requests
from pydantic import BaseModel, Field
from pydantic.json_schema import models_json_schema
@@ -59,7 +58,7 @@ class LLMService:
self,
messages: List[Dict[str, str]],
max_tokens: int = 1024,
temperature: float = 0.7,
temperature: float = 0.1,
# TODO: Consider adding a parameter like response_format_type: Optional[str] = None
# if the LLM API supports forcing JSON output (e.g., { "type": "json_object" })
) -> Optional[str]:
@@ -114,7 +113,7 @@ class LLMService:
pydantic_model_class: type[BaseModel],
prompt_instruction: Optional[str] = None,
max_tokens: int = 1024,
temperature: float = 0.7
temperature: float = 0.1
) -> Optional[Dict[str, Any]]:
"""
根据给定的Pydantic模型类生成JSON Schema,并调用LLM生成符合该Schema的参数字典。
@@ -178,6 +177,65 @@ class LLMService:
return None
def generate_data_from_schema(
self,
schema_dict: dict,
prompt_instruction: Optional[str] = None,
max_tokens: int = 1024,
temperature: float = 0.1
) -> Optional[Dict[str, Any]]:
"""
根据给定的JSON Schema字典,调用LLM生成符合该Schema的数据对象。
"""
try:
schema_str = json.dumps(schema_dict, indent=2, ensure_ascii=False)
logger.debug(f"LLMService.generate_data_from_schema: 使用的JSON Schema:\n{schema_str}")
system_prompt = (
"你是一个API测试数据生成助手。你的任务是根据用户提供的JSON Schema和额外指令,"
"生成一个符合该Schema的JSON对象。请确保你的输出严格是一个JSON对象,"
"不包含任何额外的解释、注释或Markdown标记。"
)
user_prompt_content = f"请为以下JSON Schema生成一个有效的JSON对象实例:\n\n```json\n{schema_str}\n```\n"
if prompt_instruction:
user_prompt_content += f"\n请遵循以下额外指令:\n{prompt_instruction}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_content}
]
assistant_response_content = self._execute_chat_completion_request(
messages=messages,
max_tokens=max_tokens,
temperature=temperature
)
if assistant_response_content:
# 尝试从返回内容中提取JSON部分
json_match = re.search(r'```json\n(.*?)\n```', assistant_response_content, re.DOTALL)
if json_match:
json_str = json_match.group(1)
else:
first_brace = assistant_response_content.find('{')
last_brace = assistant_response_content.rfind('}')
if first_brace != -1 and last_brace != -1 and last_brace > first_brace:
json_str = assistant_response_content[first_brace : last_brace+1]
else:
json_str = assistant_response_content
try:
generated_data = json.loads(json_str)
logger.info("成功从LLM生成并解析了数据。")
return generated_data
except json.JSONDecodeError as e_json:
logger.error(f"无法将LLM响应解析为JSON: {e_json}\n原始响应片段: '{json_str[:500]}'")
else:
logger.warning("从LLM获取的响应内容为空或请求失败。")
except Exception as e:
logger.error(f"执行LLM数据生成时发生未知错误: {e}", exc_info=True)
return None
# --- 示例用法 (用于模块内测试) ---
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG,
+106 -69
View File
@@ -1029,85 +1029,117 @@ class APITestOrchestrator:
# 1. 处理路径参数
path_param_specs = [p for p in parameters if p.get('in') == 'path']
for param_spec in path_param_specs:
name = param_spec.get('name')
if not name: continue
if path_param_specs:
should_use_llm = self._should_use_llm_for_param_type("path_params", test_case_instance)
if should_use_llm and self.llm_service:
self.logger.info(f"Attempting LLM generation for path parameter '{name}' in '{operation_id}'")
# generated_value = self.llm_service.generate_data_for_parameter(param_spec, endpoint_spec, "path")
# initial_path_params[name] = generated_value if generated_value is not None else f"llm_placeholder_for_{name}"
initial_path_params[name] = f"llm_path_{name}" # Placeholder
else:
if 'example' in param_spec:
initial_path_params[name] = param_spec['example']
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_path_params[name] = param_spec['schema']['example'] # OpenAPI 3.0 `parameter.schema.example`
elif 'default' in param_spec.get('schema', {}):
initial_path_params[name] = param_spec['schema']['default']
elif 'default' in param_spec: # OpenAPI 2.0 `parameter.default`
initial_path_params[name] = param_spec['default']
self.logger.info(f"Attempting LLM generation for path parameters in '{operation_id}'")
path_schema, path_model_name = self._build_object_schema_for_params(path_param_specs, f"{operation_id}_PathParams")
if path_schema:
llm_path_params = self.llm_service.generate_data_from_schema(
path_schema,
prompt_instruction=None,
max_tokens=256,
temperature=0.1
)
if llm_path_params:
initial_path_params = llm_path_params
else:
self.logger.warning(f"LLM failed to generate path params for '{operation_id}', fallback to default.")
else:
schema = param_spec.get('schema', {})
param_type = schema.get('type', 'string')
if param_type == 'integer': initial_path_params[name] = 123
elif param_type == 'number': initial_path_params[name] = 1.23
elif param_type == 'boolean': initial_path_params[name] = True
elif param_type == 'string' and schema.get('format') == 'uuid': initial_path_params[name] = str(UUID(int=0)) # Example UUID
elif param_type == 'string' and schema.get('format') == 'date': initial_path_params[name] = dt.date.today().isoformat()
elif param_type == 'string' and schema.get('format') == 'date-time': initial_path_params[name] = dt.datetime.now().isoformat()
else: initial_path_params[name] = f"param_{name}"
self.logger.debug(f"Initial path param for '{operation_id}': {name} = {initial_path_params.get(name)}")
self.logger.warning(f"Failed to build schema for path params in '{operation_id}', fallback to default.")
if not initial_path_params: # fallback
for param_spec in path_param_specs:
name = param_spec.get('name')
if not name: continue
if 'example' in param_spec:
initial_path_params[name] = param_spec['example']
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_path_params[name] = param_spec['schema']['example'] # OpenAPI 3.0 `parameter.schema.example`
elif 'default' in param_spec.get('schema', {}):
initial_path_params[name] = param_spec['schema']['default']
elif 'default' in param_spec: # OpenAPI 2.0 `parameter.default`
initial_path_params[name] = param_spec['default']
else:
schema = param_spec.get('schema', {})
param_type = schema.get('type', 'string')
if param_type == 'integer': initial_path_params[name] = 123
elif param_type == 'number': initial_path_params[name] = 1.23
elif param_type == 'boolean': initial_path_params[name] = True
elif param_type == 'string' and schema.get('format') == 'uuid': initial_path_params[name] = str(UUID(int=0)) # Example UUID
elif param_type == 'string' and schema.get('format') == 'date': initial_path_params[name] = dt.date.today().isoformat()
elif param_type == 'string' and schema.get('format') == 'date-time': initial_path_params[name] = dt.datetime.now().isoformat()
else: initial_path_params[name] = f"param_{name}"
self.logger.debug(f"Initial path param for '{operation_id}': {initial_path_params}")
# 2. 处理查询参数
query_param_specs = [p for p in parameters if p.get('in') == 'query']
for param_spec in query_param_specs:
name = param_spec.get('name')
if not name: continue
if query_param_specs:
should_use_llm = self._should_use_llm_for_param_type("query_params", test_case_instance)
if should_use_llm and self.llm_service:
self.logger.info(f"Attempting LLM generation for query parameter '{name}' in '{operation_id}'")
initial_query_params[name] = f"llm_query_{name}" # Placeholder
else:
if 'example' in param_spec:
initial_query_params[name] = param_spec['example']
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_query_params[name] = param_spec['schema']['example']
elif 'default' in param_spec.get('schema', {}):
initial_query_params[name] = param_spec['schema']['default']
elif 'default' in param_spec:
initial_query_params[name] = param_spec['default']
else:
initial_query_params[name] = f"query_val_{name}" # Simplified default
self.logger.debug(f"Initial query param for '{operation_id}': {name} = {initial_query_params.get(name)}")
self.logger.info(f"Attempting LLM generation for query parameters in '{operation_id}'")
query_schema, query_model_name = self._build_object_schema_for_params(query_param_specs, f"{operation_id}_QueryParams")
if query_schema:
llm_query_params = self.llm_service.generate_data_from_schema(
query_schema,
prompt_instruction=None,
max_tokens=512,
temperature=0.1
)
if llm_query_params:
initial_query_params = llm_query_params
else:
self.logger.warning(f"LLM failed to generate query params for '{operation_id}', fallback to default.")
if not initial_query_params: # fallback
for param_spec in query_param_specs:
name = param_spec.get('name')
if not name: continue
if 'example' in param_spec:
initial_query_params[name] = param_spec['example']
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_query_params[name] = param_spec['schema']['example']
elif 'default' in param_spec.get('schema', {}):
initial_query_params[name] = param_spec['schema']['default']
elif 'default' in param_spec:
initial_query_params[name] = param_spec['default']
else:
initial_query_params[name] = f"query_val_{name}"
self.logger.debug(f"Initial query param for '{operation_id}': {initial_query_params}")
# 3. 处理请求头参数 (包括规范定义的和标准的 Content-Type/Accept)
header_param_specs = [p for p in parameters if p.get('in') == 'header']
for param_spec in header_param_specs:
name = param_spec.get('name')
if not name: continue
# 标准头 Content-Type 和 Accept 会在后面专门处理
if name.lower() in ['content-type', 'accept', 'authorization']:
self.logger.debug(f"Skipping standard header '{name}' in parameter processing for '{operation_id}'. It will be handled separately.")
continue
custom_header_param_specs = [p for p in header_param_specs if p.get('name', '').lower() not in ['content-type', 'accept', 'authorization']]
if custom_header_param_specs:
should_use_llm = self._should_use_llm_for_param_type("headers", test_case_instance)
if should_use_llm and self.llm_service:
self.logger.info(f"Attempting LLM generation for header '{name}' in '{operation_id}'")
initial_headers[name] = f"llm_header_{name}" # Placeholder
else:
if 'example' in param_spec:
initial_headers[name] = str(param_spec['example'])
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_headers[name] = str(param_spec['schema']['example'])
elif 'default' in param_spec.get('schema', {}):
initial_headers[name] = str(param_spec['schema']['default'])
elif 'default' in param_spec:
initial_headers[name] = str(param_spec['default'])
else:
initial_headers[name] = f"header_val_{name}"
self.logger.debug(f"Initial custom header param for '{operation_id}': {name} = {initial_headers.get(name)}")
self.logger.info(f"Attempting LLM generation for header parameters in '{operation_id}'")
header_schema, header_model_name = self._build_object_schema_for_params(custom_header_param_specs, f"{operation_id}_HeaderParams")
if header_schema:
llm_header_params = self.llm_service.generate_data_from_schema(
header_schema,
prompt_instruction=None,
max_tokens=256,
temperature=0.1
)
if llm_header_params:
for k, v in llm_header_params.items():
initial_headers[k] = str(v)
else:
self.logger.warning(f"LLM failed to generate header params for '{operation_id}', fallback to default.")
if not any(k for k in initial_headers if k.lower() not in ['content-type', 'accept', 'authorization']): # fallback
for param_spec in custom_header_param_specs:
name = param_spec.get('name')
if not name: continue
if 'example' in param_spec:
initial_headers[name] = str(param_spec['example'])
elif param_spec.get('schema') and 'example' in param_spec['schema']:
initial_headers[name] = str(param_spec['schema']['example'])
elif 'default' in param_spec.get('schema', {}):
initial_headers[name] = str(param_spec['schema']['default'])
elif 'default' in param_spec:
initial_headers[name] = str(param_spec['default'])
else:
initial_headers[name] = f"header_val_{name}"
self.logger.debug(f"Initial custom header param for '{operation_id}': {initial_headers}")
# 3.1 设置 Content-Type
# 优先从 requestBody.content 获取 (OpenAPI 3.x)
@@ -1163,7 +1195,12 @@ class APITestOrchestrator:
should_use_llm_for_body = self._should_use_llm_for_param_type("body", test_case_instance)
if should_use_llm_for_body and self.llm_service:
self.logger.info(f"Attempting LLM generation for request body of '{operation_id}' with schema...")
initial_body = self.llm_service.generate_data_from_schema(request_body_schema, endpoint_spec, "requestBody")
initial_body = self.llm_service.generate_data_from_schema(
request_body_schema,
prompt_instruction=None, # 如有自定义指令可替换
max_tokens=1024,
temperature=0.1
)
if initial_body is None:
self.logger.warning(f"LLM failed to generate request body for '{operation_id}'. Falling back to default schema generator.")
initial_body = self._generate_data_from_schema(request_body_schema, context_name=f"{operation_id}_body", operation_id=operation_id)