Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion WHartTest_Django/knowledge/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,9 @@ def _rewrite_query(self, query: str) -> Optional[str]:
model=config.name,
api_key=config.api_key,
base_url=config.api_url,
temperature=0.3,
temperature=1
if (config.name or "").lower().startswith("kimi-k3")
else 0.3,
max_tokens=100,
timeout=15,
)
Expand Down
2 changes: 2 additions & 0 deletions WHartTest_Django/langgraph_integration/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ def create_llm_instance(active_config, temperature=0.7):
- max_retries: 最大重试次数,处理临时网络问题
"""
model_identifier = active_config.name or "gpt-3.5-turbo"
if model_identifier.lower().startswith("kimi-k3"):
temperature = 1
provider = (getattr(active_config, "provider", None) or "openai_compatible").strip()

# 从配置获取超时设置,默认120秒(LLM响应可能较慢)
Expand Down
101 changes: 71 additions & 30 deletions WHartTest_Django/requirements/services.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import json
import re
import logging
import json
import math
import re
from string import Template
from typing import List, Dict, Any, Optional
from django.conf import settings
Expand All @@ -11,23 +12,44 @@
from .models import RequirementDocument, RequirementModule, DocumentImage
from prompts.models import UserPrompt

logger = logging.getLogger(__name__)


def create_llm_instance(active_config, temperature=0.1):
logger = logging.getLogger(__name__)


def normalize_score(value, default=70):
"""Convert an LLM-provided score to a database-safe integer in 0..100."""
try:
if value is None or isinstance(value, bool):
raise ValueError
score = float(value)
if not math.isfinite(score):
raise ValueError
except (TypeError, ValueError):
score = float(default)

return max(0, min(100, int(round(score))))


def create_llm_instance(active_config, temperature=0.1):
"""
根据配置创建LLM实例
统一使用OpenAI兼容格式,支持所有兼容的服务商
"""
model_identifier = active_config.name or "gpt-3.5-turbo"

llm_kwargs = {
"""
model_identifier = active_config.name or "gpt-3.5-turbo"
if model_identifier.lower().startswith("kimi-k3"):
temperature = 1

configured_retries = getattr(active_config, "max_retries", None)
max_retries = 3 if configured_retries is None else max(0, int(configured_retries))
configured_timeout = getattr(active_config, "request_timeout", None)
request_timeout = 300 if configured_timeout is None else max(1, int(configured_timeout))

llm_kwargs = {
"model": model_identifier,
"temperature": temperature,
"api_key": active_config.api_key,
"base_url": active_config.api_url,
"max_retries": 3,
"timeout": 120,
"max_retries": max_retries,
"timeout": request_timeout,
}
llm = ChatOpenAI(**llm_kwargs)
logger.info(
Expand Down Expand Up @@ -86,12 +108,10 @@ def safe_llm_invoke(llm, messages, max_retries=3, retry_delay=2):
time.sleep(retry_delay * (attempt + 1))
continue
raise
except Exception as e:
last_error = e
logger.warning(f"LLM 调用失败: {e},尝试重试 ({attempt + 1}/{max_retries})")
if attempt < max_retries - 1:
time.sleep(retry_delay * (attempt + 1))
continue
except Exception:
# ChatOpenAI already applies the configured network retries. Retrying
# again here multiplies a 300-second timeout into hour-long stalls.
raise

# 所有重试都失败
raise last_error or Exception("LLM 调用失败,所有重试都未成功")
Expand Down Expand Up @@ -3119,8 +3139,11 @@ def analyze_document_comprehensive(
for future in as_completed(future_to_analysis):
analysis_name, display_name = future_to_analysis[future]
try:
result = future.result()
results[analysis_name] = result
result = future.result()
result["overall_score"] = normalize_score(
result.get("overall_score"), 70
)
results[analysis_name] = result
# 收集图片警告(如果有)
if result.get("image_warning") and not image_warning:
image_warning = result.get("image_warning")
Expand Down Expand Up @@ -3221,8 +3244,9 @@ def _generate_comprehensive_report_v2(self, analyses: dict) -> dict:
clarity,
logic,
]:
score = analysis.get("overall_score", 70)
scores.append(score)
score = normalize_score(analysis.get("overall_score"), 70)
analysis["overall_score"] = score
scores.append(score)

overall_score = int(sum(scores) / len(scores)) if scores else 70

Expand Down Expand Up @@ -3878,10 +3902,11 @@ def progress_callback(
except Exception as e:
logger.error(f"评审失败: {e}")

# 更新失败状态
if "review_report" in locals():
review_report.status = "failed"
review_report.save()
# 更新失败状态
if "review_report" in locals():
# Avoid saving other dirty fields (for example an LLM-provided
# None score) while recording the failure state.
ReviewReport.objects.filter(pk=review_report.pk).update(status="failed")

document.status = "failed"
document.save()
Expand All @@ -3893,7 +3918,9 @@ def _update_review_report(
):
"""更新评审报告基本信息和专项分析详情"""
review_report.overall_rating = analysis_result.get("overall_rating", "average")
review_report.completion_score = analysis_result.get("overall_score", 0)
review_report.completion_score = normalize_score(
analysis_result.get("overall_score"), 70
)
review_report.total_issues = analysis_result.get("total_issues", 0)
review_report.high_priority_issues = analysis_result.get(
"high_priority_issues", 0
Expand All @@ -3910,8 +3937,22 @@ def _update_review_report(
)

# 保存专项分析详情(包含issues, strengths, recommendations等完整数据)
specialized_analyses = analysis_result.get("specialized_analyses", {})
review_report.specialized_analyses = specialized_analyses
specialized_analyses = analysis_result.get("specialized_analyses", {})
analysis_keys = (
"completeness_analysis",
"consistency_analysis",
"clarity_analysis",
"testability_analysis",
"feasibility_analysis",
"logic_analysis",
)
for key in analysis_keys:
detail = specialized_analyses.get(key)
if not isinstance(detail, dict):
detail = {}
specialized_analyses[key] = detail
detail["overall_score"] = normalize_score(detail.get("overall_score"), 70)
review_report.specialized_analyses = specialized_analyses

# 同时保存各专项分析的分数到独立字段
review_report.completeness_score = specialized_analyses.get(
Expand Down
18 changes: 12 additions & 6 deletions WHartTest_Django/requirements/tasks.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""
需求评审异步任务
"""
import logging
from celery import shared_task
from django.utils import timezone
import logging
from celery import shared_task
from django.conf import settings
from django.utils import timezone

logger = logging.getLogger(__name__)


@shared_task(bind=True, name='requirements.execute_requirement_review')
def execute_requirement_review(self, document_id, analysis_options=None, review_type='comprehensive', user_id=None):
@shared_task(
bind=True,
name='requirements.execute_requirement_review',
time_limit=settings.REQUIREMENT_REVIEW_TASK_TIME_LIMIT,
soft_time_limit=settings.REQUIREMENT_REVIEW_TASK_SOFT_TIME_LIMIT,
)
def execute_requirement_review(self, document_id, analysis_options=None, review_type='comprehensive', user_id=None):
"""
异步执行需求评审任务

Expand Down Expand Up @@ -85,4 +91,4 @@ def execute_requirement_review(self, document_id, analysis_options=None, review_
return {
'status': 'error',
'message': str(e)
}
}
48 changes: 48 additions & 0 deletions WHartTest_Django/requirements/test_review_regressions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.test import SimpleTestCase

from requirements.services import (
RequirementReviewService,
normalize_score,
)


class NormalizeScoreTests(SimpleTestCase):
def test_normalizes_missing_strings_and_out_of_range_values(self):
self.assertEqual(normalize_score(None), 70)
self.assertEqual(normalize_score("62"), 62)
self.assertEqual(normalize_score(-5), 0)
self.assertEqual(normalize_score(105), 100)
self.assertEqual(normalize_score(True), 70)

def test_report_fields_never_receive_null_scores(self):
class Report:
def save(self):
self.saved = True

report = Report()
result = {
"overall_score": "67",
"overall_rating": "needs_improvement",
"recommendations": [],
"specialized_analyses": {
"completeness_analysis": {"overall_score": 70},
"consistency_analysis": {"overall_score": "62"},
"clarity_analysis": {"overall_score": 78.4},
"testability_analysis": {"overall_score": 52},
"feasibility_analysis": {"overall_score": None},
"logic_analysis": {"overall_score": 70},
},
}

service = RequirementReviewService.__new__(RequirementReviewService)
service._update_review_report(report, result)

self.assertTrue(report.saved)
self.assertEqual(report.completion_score, 67)
self.assertEqual(report.consistency_score, 62)
self.assertEqual(report.clarity_score, 78)
self.assertEqual(report.feasibility_score, 70)
self.assertEqual(
report.specialized_analyses["feasibility_analysis"]["overall_score"],
70,
)
16 changes: 14 additions & 2 deletions WHartTest_Django/wharttest_django/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,20 @@ def setup_huggingface_env():

# Celery任务配置
CELERY_TASK_TRACK_STARTED = True # 追踪任务开始状态。
CELERY_TASK_TIME_LIMIT = 30 * 60 # 任务硬超时(秒,30 分钟)。
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 任务软超时(秒,25 分钟)。
CELERY_TASK_TIME_LIMIT = 30 * 60 # 任务硬超时(秒,30 分钟)。
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # 任务软超时(秒,25 分钟)。

# Requirement reviews make several long-running LLM calls. Keep their larger
# limit task-specific so unrelated background work still fails promptly.
REQUIREMENT_REVIEW_TASK_TIME_LIMIT = int(
os.environ.get("REQUIREMENT_REVIEW_TASK_TIME_LIMIT", str(3 * 60 * 60))
)
REQUIREMENT_REVIEW_TASK_SOFT_TIME_LIMIT = int(
os.environ.get(
"REQUIREMENT_REVIEW_TASK_SOFT_TIME_LIMIT",
str(REQUIREMENT_REVIEW_TASK_TIME_LIMIT - 5 * 60),
)
)

# Celery Worker配置
CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # Worker 预取任务数量。
Expand Down
Loading