codegnipy 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegnipy/__init__.py +190 -0
- codegnipy/cli.py +153 -0
- codegnipy/decorator.py +151 -0
- codegnipy/determinism.py +631 -0
- codegnipy/memory.py +276 -0
- codegnipy/providers.py +1160 -0
- codegnipy/reflection.py +244 -0
- codegnipy/runtime.py +197 -0
- codegnipy/scheduler.py +498 -0
- codegnipy/streaming.py +387 -0
- codegnipy/tools.py +481 -0
- codegnipy/transformer.py +155 -0
- codegnipy/validation.py +961 -0
- codegnipy-0.0.1.dist-info/METADATA +417 -0
- codegnipy-0.0.1.dist-info/RECORD +19 -0
- codegnipy-0.0.1.dist-info/WHEEL +5 -0
- codegnipy-0.0.1.dist-info/entry_points.txt +2 -0
- codegnipy-0.0.1.dist-info/licenses/LICENSE +21 -0
- codegnipy-0.0.1.dist-info/top_level.txt +1 -0
codegnipy/validation.py
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codegnipy 外部验证模块
|
|
3
|
+
|
|
4
|
+
提供外部验证集成,包括 Web 搜索验证、知识图谱查询、事实核查 API。
|
|
5
|
+
增强幻觉检测的准确率。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import re
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any, Dict, List, Optional, TYPE_CHECKING, cast
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .runtime import CognitiveContext
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExternalValidationStatus(Enum):
|
|
20
|
+
"""外部验证状态"""
|
|
21
|
+
VERIFIED = "verified" # 已验证为真
|
|
22
|
+
REFUTED = "refuted" # 已验证为假
|
|
23
|
+
UNCERTAIN = "uncertain" # 无法确定
|
|
24
|
+
ERROR = "error" # 验证出错
|
|
25
|
+
UNAVAILABLE = "unavailable" # 服务不可用
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Evidence:
|
|
30
|
+
"""验证证据"""
|
|
31
|
+
source: str # 来源名称
|
|
32
|
+
url: Optional[str] = None # 来源 URL
|
|
33
|
+
snippet: str = "" # 相关片段
|
|
34
|
+
relevance: float = 1.0 # 相关性评分 (0-1)
|
|
35
|
+
supports_claim: Optional[bool] = None # True=支持, False=反驳, None=不确定
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ExternalValidationResult:
|
|
40
|
+
"""外部验证结果"""
|
|
41
|
+
claim: str
|
|
42
|
+
status: ExternalValidationStatus
|
|
43
|
+
confidence: float # 0-1
|
|
44
|
+
evidences: List[Evidence] = field(default_factory=list)
|
|
45
|
+
summary: str = ""
|
|
46
|
+
raw_response: Optional[Dict[str, Any]] = None
|
|
47
|
+
error: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseValidator(ABC):
|
|
51
|
+
"""外部验证器抽象基类"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def validate_async(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
55
|
+
"""
|
|
56
|
+
异步验证声明
|
|
57
|
+
|
|
58
|
+
参数:
|
|
59
|
+
claim: 要验证的声明文本
|
|
60
|
+
context: 认知上下文(可选)
|
|
61
|
+
返回:
|
|
62
|
+
ExternalValidationResult 对象
|
|
63
|
+
"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def validate(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
67
|
+
"""
|
|
68
|
+
同步验证声明
|
|
69
|
+
|
|
70
|
+
参数:
|
|
71
|
+
claim: 要验证的声明文本
|
|
72
|
+
context: 认知上下文(可选)
|
|
73
|
+
返回:
|
|
74
|
+
ExternalValidationResult 对象
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
loop = asyncio.get_running_loop()
|
|
78
|
+
except RuntimeError:
|
|
79
|
+
loop = None
|
|
80
|
+
|
|
81
|
+
if loop and loop.is_running():
|
|
82
|
+
# 如果已有事件循环,创建新线程运行
|
|
83
|
+
import concurrent.futures
|
|
84
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
85
|
+
future = executor.submit(
|
|
86
|
+
asyncio.run,
|
|
87
|
+
self.validate_async(claim, context)
|
|
88
|
+
)
|
|
89
|
+
return future.result()
|
|
90
|
+
else:
|
|
91
|
+
return asyncio.run(self.validate_async(claim, context))
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def is_available(self) -> bool:
|
|
95
|
+
"""检查验证器是否可用"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
"""验证器名称"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ============ Web 搜索验证器 ============
|
|
106
|
+
|
|
107
|
+
class WebSearchValidator(BaseValidator):
|
|
108
|
+
"""
|
|
109
|
+
Web 搜索验证器
|
|
110
|
+
|
|
111
|
+
使用搜索引擎验证声明,支持 DuckDuckGo(免费)和 Bing API。
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
engine: str = "duckduckgo",
|
|
117
|
+
api_key: Optional[str] = None,
|
|
118
|
+
max_results: int = 5,
|
|
119
|
+
timeout: float = 10.0
|
|
120
|
+
):
|
|
121
|
+
"""
|
|
122
|
+
初始化 Web 搜索验证器
|
|
123
|
+
|
|
124
|
+
参数:
|
|
125
|
+
engine: 搜索引擎 ("duckduckgo" 或 "bing")
|
|
126
|
+
api_key: Bing API 密钥(仅 Bing 需要)
|
|
127
|
+
max_results: 最大返回结果数
|
|
128
|
+
timeout: 请求超时时间
|
|
129
|
+
"""
|
|
130
|
+
self.engine = engine.lower()
|
|
131
|
+
self.api_key = api_key
|
|
132
|
+
self.max_results = max_results
|
|
133
|
+
self.timeout = timeout
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def name(self) -> str:
|
|
137
|
+
return f"web_search_{self.engine}"
|
|
138
|
+
|
|
139
|
+
def is_available(self) -> bool:
|
|
140
|
+
if self.engine == "duckduckgo":
|
|
141
|
+
return True # DuckDuckGo 无需 API 密钥
|
|
142
|
+
elif self.engine == "bing":
|
|
143
|
+
return self.api_key is not None
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
async def validate_async(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
147
|
+
"""使用 Web 搜索验证声明"""
|
|
148
|
+
if not self.is_available():
|
|
149
|
+
return ExternalValidationResult(
|
|
150
|
+
claim=claim,
|
|
151
|
+
status=ExternalValidationStatus.UNAVAILABLE,
|
|
152
|
+
confidence=0.0,
|
|
153
|
+
error=f"验证器不可用: {self.name}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# 提取搜索关键词
|
|
158
|
+
keywords = self._extract_keywords(claim)
|
|
159
|
+
search_query = " ".join(keywords)
|
|
160
|
+
|
|
161
|
+
if self.engine == "duckduckgo":
|
|
162
|
+
results = await self._search_duckduckgo(search_query)
|
|
163
|
+
else:
|
|
164
|
+
results = await self._search_bing(search_query)
|
|
165
|
+
|
|
166
|
+
# 分析搜索结果
|
|
167
|
+
evidences = self._analyze_results(claim, results)
|
|
168
|
+
|
|
169
|
+
# 计算验证状态和置信度
|
|
170
|
+
status, confidence, summary = self._compute_verdict(claim, evidences)
|
|
171
|
+
|
|
172
|
+
return ExternalValidationResult(
|
|
173
|
+
claim=claim,
|
|
174
|
+
status=status,
|
|
175
|
+
confidence=confidence,
|
|
176
|
+
evidences=evidences,
|
|
177
|
+
summary=summary,
|
|
178
|
+
raw_response={"results": results}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return ExternalValidationResult(
|
|
183
|
+
claim=claim,
|
|
184
|
+
status=ExternalValidationStatus.ERROR,
|
|
185
|
+
confidence=0.0,
|
|
186
|
+
error=str(e)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _extract_keywords(self, text: str) -> List[str]:
|
|
190
|
+
"""提取关键词"""
|
|
191
|
+
# 移除停用词(简单实现)
|
|
192
|
+
stop_words = {"的", "是", "在", "和", "了", "有", "不", "这", "我", "他", "她", "它",
|
|
193
|
+
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
|
|
194
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
195
|
+
"should", "may", "might", "must", "can", "to", "of", "in", "for",
|
|
196
|
+
"on", "with", "at", "by", "from", "as", "into", "through"}
|
|
197
|
+
|
|
198
|
+
# 分词(简单实现:按空格和标点分割)
|
|
199
|
+
words = re.findall(r'[\w\u4e00-\u9fff]+', text.lower())
|
|
200
|
+
|
|
201
|
+
# 过滤停用词并保留有意义的词
|
|
202
|
+
keywords = [w for w in words if w not in stop_words and len(w) > 1]
|
|
203
|
+
|
|
204
|
+
return keywords[:10] # 最多返回 10 个关键词
|
|
205
|
+
|
|
206
|
+
async def _search_duckduckgo(self, query: str) -> List[Dict[str, Any]]:
|
|
207
|
+
"""使用 DuckDuckGo 搜索"""
|
|
208
|
+
import aiohttp
|
|
209
|
+
|
|
210
|
+
# 使用 DuckDuckGo Instant Answer API
|
|
211
|
+
url = "https://api.duckduckgo.com/"
|
|
212
|
+
params = {
|
|
213
|
+
"q": query,
|
|
214
|
+
"format": "json",
|
|
215
|
+
"no_html": 1,
|
|
216
|
+
"skip_disambig": 1
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async with aiohttp.ClientSession() as session:
|
|
220
|
+
async with session.get(url, params=params, timeout=self.timeout) as response:
|
|
221
|
+
data = await response.json()
|
|
222
|
+
|
|
223
|
+
results = []
|
|
224
|
+
|
|
225
|
+
# 解析相关主题
|
|
226
|
+
if "RelatedTopics" in data:
|
|
227
|
+
for topic in data["RelatedTopics"][:self.max_results]:
|
|
228
|
+
if isinstance(topic, dict):
|
|
229
|
+
results.append({
|
|
230
|
+
"title": topic.get("Text", "")[:100],
|
|
231
|
+
"snippet": topic.get("Text", ""),
|
|
232
|
+
"url": topic.get("FirstURL", "")
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
# 解析摘要
|
|
236
|
+
if data.get("Abstract"):
|
|
237
|
+
results.insert(0, {
|
|
238
|
+
"title": data.get("Heading", "Summary"),
|
|
239
|
+
"snippet": data.get("Abstract", ""),
|
|
240
|
+
"url": data.get("AbstractURL", "")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
return results
|
|
244
|
+
|
|
245
|
+
async def _search_bing(self, query: str) -> List[Dict[str, Any]]:
|
|
246
|
+
"""使用 Bing 搜索 API"""
|
|
247
|
+
import aiohttp
|
|
248
|
+
|
|
249
|
+
url = "https://api.bing.microsoft.com/v7.0/search"
|
|
250
|
+
headers = {"Ocp-Apim-Subscription-Key": self.api_key}
|
|
251
|
+
params = {
|
|
252
|
+
"q": query,
|
|
253
|
+
"count": self.max_results,
|
|
254
|
+
"responseFilter": "Webpages"
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async with aiohttp.ClientSession() as session:
|
|
258
|
+
async with session.get(url, headers=headers, params=params, timeout=self.timeout) as response:
|
|
259
|
+
data = await response.json()
|
|
260
|
+
|
|
261
|
+
results = []
|
|
262
|
+
if "webPages" in data and "value" in data["webPages"]:
|
|
263
|
+
for item in data["webPages"]["value"]:
|
|
264
|
+
results.append({
|
|
265
|
+
"title": item.get("name", ""),
|
|
266
|
+
"snippet": item.get("snippet", ""),
|
|
267
|
+
"url": item.get("url", "")
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return results
|
|
271
|
+
|
|
272
|
+
def _analyze_results(self, claim: str, results: List[Dict[str, Any]]) -> List[Evidence]:
|
|
273
|
+
"""分析搜索结果"""
|
|
274
|
+
evidences = []
|
|
275
|
+
|
|
276
|
+
for result in results:
|
|
277
|
+
snippet = result.get("snippet", "")
|
|
278
|
+
title = result.get("title", "")
|
|
279
|
+
|
|
280
|
+
# 简单的相关性判断
|
|
281
|
+
relevance = self._compute_relevance(claim, title + " " + snippet)
|
|
282
|
+
|
|
283
|
+
# 判断是否支持声明(简单实现)
|
|
284
|
+
supports = self._check_support(claim, snippet)
|
|
285
|
+
|
|
286
|
+
evidences.append(Evidence(
|
|
287
|
+
source=result.get("title", "Unknown"),
|
|
288
|
+
url=result.get("url"),
|
|
289
|
+
snippet=snippet[:200] if snippet else "",
|
|
290
|
+
relevance=relevance,
|
|
291
|
+
supports_claim=supports
|
|
292
|
+
))
|
|
293
|
+
|
|
294
|
+
return evidences
|
|
295
|
+
|
|
296
|
+
def _compute_relevance(self, claim: str, text: str) -> float:
|
|
297
|
+
"""计算相关性"""
|
|
298
|
+
claim_words = set(re.findall(r'[\w\u4e00-\u9fff]+', claim.lower()))
|
|
299
|
+
text_words = set(re.findall(r'[\w\u4e00-\u9fff]+', text.lower()))
|
|
300
|
+
|
|
301
|
+
if not claim_words:
|
|
302
|
+
return 0.0
|
|
303
|
+
|
|
304
|
+
intersection = claim_words & text_words
|
|
305
|
+
return len(intersection) / len(claim_words)
|
|
306
|
+
|
|
307
|
+
def _check_support(self, claim: str, text: str) -> Optional[bool]:
|
|
308
|
+
"""检查文本是否支持声明"""
|
|
309
|
+
# 简单实现:检查否定词
|
|
310
|
+
negation_patterns = [
|
|
311
|
+
r'\b不\s*(正确|真实|存在|属实)\b',
|
|
312
|
+
r'\b(false|fake|incorrect|wrong|not true)\b',
|
|
313
|
+
r'\b谣言\b',
|
|
314
|
+
r'\b辟谣\b'
|
|
315
|
+
]
|
|
316
|
+
|
|
317
|
+
text_lower = text.lower()
|
|
318
|
+
for pattern in negation_patterns:
|
|
319
|
+
if re.search(pattern, text_lower, re.IGNORECASE):
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
# 检查确认词
|
|
323
|
+
confirm_patterns = [
|
|
324
|
+
r'\b(正确|真实|属实|确认)\b',
|
|
325
|
+
r'\b(true|correct|confirmed|verified)\b'
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
for pattern in confirm_patterns:
|
|
329
|
+
if re.search(pattern, text_lower, re.IGNORECASE):
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
return None # 无法确定
|
|
333
|
+
|
|
334
|
+
def _compute_verdict(
|
|
335
|
+
self,
|
|
336
|
+
claim: str,
|
|
337
|
+
evidences: List[Evidence]
|
|
338
|
+
) -> tuple[ExternalValidationStatus, float, str]:
|
|
339
|
+
"""计算验证结论"""
|
|
340
|
+
if not evidences:
|
|
341
|
+
return ExternalValidationStatus.UNCERTAIN, 0.0, "未找到相关证据"
|
|
342
|
+
|
|
343
|
+
# 加权计算
|
|
344
|
+
support_score = 0.0
|
|
345
|
+
refute_score = 0.0
|
|
346
|
+
total_weight = 0.0
|
|
347
|
+
|
|
348
|
+
for evidence in evidences:
|
|
349
|
+
weight = evidence.relevance
|
|
350
|
+
total_weight += weight
|
|
351
|
+
|
|
352
|
+
if evidence.supports_claim is True:
|
|
353
|
+
support_score += weight
|
|
354
|
+
elif evidence.supports_claim is False:
|
|
355
|
+
refute_score += weight
|
|
356
|
+
|
|
357
|
+
if total_weight == 0:
|
|
358
|
+
return ExternalValidationStatus.UNCERTAIN, 0.0, "无法确定相关性"
|
|
359
|
+
|
|
360
|
+
support_ratio = support_score / total_weight
|
|
361
|
+
refute_ratio = refute_score / total_weight
|
|
362
|
+
|
|
363
|
+
# 生成摘要
|
|
364
|
+
supporting = sum(1 for e in evidences if e.supports_claim is True)
|
|
365
|
+
refuting = sum(1 for e in evidences if e.supports_claim is False)
|
|
366
|
+
|
|
367
|
+
if support_ratio > 0.6:
|
|
368
|
+
return (
|
|
369
|
+
ExternalValidationStatus.VERIFIED,
|
|
370
|
+
support_ratio,
|
|
371
|
+
f"找到 {supporting} 个支持性证据,{refuting} 个反驳性证据"
|
|
372
|
+
)
|
|
373
|
+
elif refute_ratio > 0.4:
|
|
374
|
+
return (
|
|
375
|
+
ExternalValidationStatus.REFUTED,
|
|
376
|
+
refute_ratio,
|
|
377
|
+
f"找到 {refuting} 个反驳性证据,{supporting} 个支持性证据"
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
return (
|
|
381
|
+
ExternalValidationStatus.UNCERTAIN,
|
|
382
|
+
0.5,
|
|
383
|
+
f"证据不足:{supporting} 个支持,{refuting} 个反驳,{len(evidences) - supporting - refuting} 个不确定"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ============ 知识图谱验证器 ============
|
|
388
|
+
|
|
389
|
+
class KnowledgeGraphValidator(BaseValidator):
|
|
390
|
+
"""
|
|
391
|
+
知识图谱验证器
|
|
392
|
+
|
|
393
|
+
使用 Wikidata SPARQL 查询验证实体和关系。
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
SPARQL_ENDPOINT = "https://query.wikidata.org/sparql"
|
|
397
|
+
|
|
398
|
+
def __init__(self, timeout: float = 15.0, language: str = "zh"):
|
|
399
|
+
"""
|
|
400
|
+
初始化知识图谱验证器
|
|
401
|
+
|
|
402
|
+
参数:
|
|
403
|
+
timeout: 请求超时时间
|
|
404
|
+
language: 语言代码
|
|
405
|
+
"""
|
|
406
|
+
self.timeout = timeout
|
|
407
|
+
self.language = language
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def name(self) -> str:
|
|
411
|
+
return "knowledge_graph_wikidata"
|
|
412
|
+
|
|
413
|
+
def is_available(self) -> bool:
|
|
414
|
+
return True # Wikidata 是公开 API
|
|
415
|
+
|
|
416
|
+
async def validate_async(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
417
|
+
"""使用知识图谱验证声明"""
|
|
418
|
+
try:
|
|
419
|
+
# 尝试提取实体
|
|
420
|
+
entities = await self._extract_entities(claim)
|
|
421
|
+
|
|
422
|
+
if not entities:
|
|
423
|
+
return ExternalValidationResult(
|
|
424
|
+
claim=claim,
|
|
425
|
+
status=ExternalValidationStatus.UNCERTAIN,
|
|
426
|
+
confidence=0.3,
|
|
427
|
+
summary="无法从声明中提取已知实体"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# 查询实体信息
|
|
431
|
+
evidences = []
|
|
432
|
+
for entity_id, entity_label in entities[:3]: # 最多查询 3 个实体
|
|
433
|
+
entity_info = await self._query_entity(entity_id)
|
|
434
|
+
if entity_info:
|
|
435
|
+
evidences.append(Evidence(
|
|
436
|
+
source=f"Wikidata: {entity_label}",
|
|
437
|
+
url=f"https://www.wikidata.org/wiki/{entity_id}",
|
|
438
|
+
snippet=entity_info.get("description", ""),
|
|
439
|
+
relevance=0.8,
|
|
440
|
+
supports_claim=None
|
|
441
|
+
))
|
|
442
|
+
|
|
443
|
+
if evidences:
|
|
444
|
+
return ExternalValidationResult(
|
|
445
|
+
claim=claim,
|
|
446
|
+
status=ExternalValidationStatus.UNCERTAIN,
|
|
447
|
+
confidence=0.5,
|
|
448
|
+
evidences=evidences,
|
|
449
|
+
summary=f"找到 {len(evidences)} 个相关实体"
|
|
450
|
+
)
|
|
451
|
+
else:
|
|
452
|
+
return ExternalValidationResult(
|
|
453
|
+
claim=claim,
|
|
454
|
+
status=ExternalValidationStatus.UNCERTAIN,
|
|
455
|
+
confidence=0.3,
|
|
456
|
+
summary="未在知识图谱中找到相关实体"
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
except Exception as e:
|
|
460
|
+
return ExternalValidationResult(
|
|
461
|
+
claim=claim,
|
|
462
|
+
status=ExternalValidationStatus.ERROR,
|
|
463
|
+
confidence=0.0,
|
|
464
|
+
error=str(e)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
async def _extract_entities(self, text: str) -> List[tuple[str, str]]:
|
|
468
|
+
"""从文本中提取 Wikidata 实体"""
|
|
469
|
+
import aiohttp
|
|
470
|
+
|
|
471
|
+
# 构建 SPARQL 查询搜索实体
|
|
472
|
+
# 使用实体标签搜索
|
|
473
|
+
query = f'''
|
|
474
|
+
SELECT ?entity ?entityLabel WHERE {{
|
|
475
|
+
?entity ?label "{text}" .
|
|
476
|
+
?entity rdfs:label ?entityLabel .
|
|
477
|
+
FILTER(LANG(?entityLabel) = "{self.language}")
|
|
478
|
+
}}
|
|
479
|
+
LIMIT 5
|
|
480
|
+
'''
|
|
481
|
+
|
|
482
|
+
# 如果文本较长,尝试搜索包含的实体
|
|
483
|
+
words = re.findall(r'[\w\u4e00-\u9fff]+', text)
|
|
484
|
+
if len(words) > 1:
|
|
485
|
+
# 搜索最可能的实体词
|
|
486
|
+
search_terms = words[:3]
|
|
487
|
+
query = f'''
|
|
488
|
+
SELECT ?entity ?entityLabel WHERE {{
|
|
489
|
+
?entity rdfs:label ?label .
|
|
490
|
+
FILTER(LANG(?label) = "{self.language}")
|
|
491
|
+
FILTER(CONTAINS(?label, "{search_terms[0]}") ||
|
|
492
|
+
?label = "{search_terms[0]}")
|
|
493
|
+
}}
|
|
494
|
+
LIMIT 5
|
|
495
|
+
'''
|
|
496
|
+
|
|
497
|
+
headers = {"Accept": "application/sparql-results+json"}
|
|
498
|
+
params = {"query": query, "format": "json"}
|
|
499
|
+
|
|
500
|
+
async with aiohttp.ClientSession() as session:
|
|
501
|
+
async with session.get(
|
|
502
|
+
self.SPARQL_ENDPOINT,
|
|
503
|
+
headers=headers,
|
|
504
|
+
params=params,
|
|
505
|
+
timeout=self.timeout
|
|
506
|
+
) as response:
|
|
507
|
+
data = await response.json()
|
|
508
|
+
|
|
509
|
+
entities = []
|
|
510
|
+
for binding in data.get("results", {}).get("bindings", []):
|
|
511
|
+
entity_uri = binding.get("entity", {}).get("value", "")
|
|
512
|
+
entity_label = binding.get("entityLabel", {}).get("value", "")
|
|
513
|
+
if entity_uri:
|
|
514
|
+
entity_id = entity_uri.split("/")[-1]
|
|
515
|
+
entities.append((entity_id, entity_label))
|
|
516
|
+
|
|
517
|
+
return entities
|
|
518
|
+
|
|
519
|
+
async def _query_entity(self, entity_id: str) -> Optional[Dict[str, Any]]:
|
|
520
|
+
"""查询实体详细信息"""
|
|
521
|
+
import aiohttp
|
|
522
|
+
|
|
523
|
+
query = f'''
|
|
524
|
+
SELECT ?description ?altLabel WHERE {{
|
|
525
|
+
wd:{entity_id} schema:description ?description .
|
|
526
|
+
OPTIONAL {{ wd:{entity_id} skos:altLabel ?altLabel . }}
|
|
527
|
+
FILTER(LANG(?description) = "{self.language}")
|
|
528
|
+
FILTER(LANG(?altLabel) = "{self.language}")
|
|
529
|
+
}}
|
|
530
|
+
LIMIT 1
|
|
531
|
+
'''
|
|
532
|
+
|
|
533
|
+
headers = {"Accept": "application/sparql-results+json"}
|
|
534
|
+
params = {"query": query, "format": "json"}
|
|
535
|
+
|
|
536
|
+
async with aiohttp.ClientSession() as session:
|
|
537
|
+
async with session.get(
|
|
538
|
+
self.SPARQL_ENDPOINT,
|
|
539
|
+
headers=headers,
|
|
540
|
+
params=params,
|
|
541
|
+
timeout=self.timeout
|
|
542
|
+
) as response:
|
|
543
|
+
data = await response.json()
|
|
544
|
+
|
|
545
|
+
bindings = data.get("results", {}).get("bindings", [])
|
|
546
|
+
if bindings:
|
|
547
|
+
return {
|
|
548
|
+
"description": bindings[0].get("description", {}).get("value", ""),
|
|
549
|
+
"alt_labels": [b.get("altLabel", {}).get("value", "") for b in bindings if b.get("altLabel")]
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# ============ 事实核查验证器 ============
|
|
556
|
+
|
|
557
|
+
class FactCheckValidator(BaseValidator):
|
|
558
|
+
"""
|
|
559
|
+
事实核查验证器
|
|
560
|
+
|
|
561
|
+
使用 Google Fact Check Tools API 验证声明。
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
API_URL = "https://factchecktools.googleapis.com/v1alpha1/claims:search"
|
|
565
|
+
|
|
566
|
+
def __init__(self, api_key: Optional[str] = None, timeout: float = 10.0):
|
|
567
|
+
"""
|
|
568
|
+
初始化事实核查验证器
|
|
569
|
+
|
|
570
|
+
参数:
|
|
571
|
+
api_key: Google Fact Check API 密钥
|
|
572
|
+
timeout: 请求超时时间
|
|
573
|
+
"""
|
|
574
|
+
self.api_key = api_key
|
|
575
|
+
self.timeout = timeout
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def name(self) -> str:
|
|
579
|
+
return "fact_check_google"
|
|
580
|
+
|
|
581
|
+
def is_available(self) -> bool:
|
|
582
|
+
return self.api_key is not None
|
|
583
|
+
|
|
584
|
+
async def validate_async(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
585
|
+
"""使用事实核查 API 验证声明"""
|
|
586
|
+
import aiohttp
|
|
587
|
+
|
|
588
|
+
if not self.is_available():
|
|
589
|
+
return ExternalValidationResult(
|
|
590
|
+
claim=claim,
|
|
591
|
+
status=ExternalValidationStatus.UNAVAILABLE,
|
|
592
|
+
confidence=0.0,
|
|
593
|
+
error="Google Fact Check API 密钥未配置"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
params = {
|
|
598
|
+
"query": claim,
|
|
599
|
+
"key": self.api_key
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async with aiohttp.ClientSession() as session:
|
|
603
|
+
async with session.get(
|
|
604
|
+
self.API_URL,
|
|
605
|
+
params=params,
|
|
606
|
+
timeout=self.timeout
|
|
607
|
+
) as response:
|
|
608
|
+
data = await response.json()
|
|
609
|
+
|
|
610
|
+
# 解析事实核查结果
|
|
611
|
+
evidences = []
|
|
612
|
+
claims = data.get("claims", [])
|
|
613
|
+
|
|
614
|
+
for claim_data in claims[:self.max_results if hasattr(self, 'max_results') else 5]:
|
|
615
|
+
claim_review = claim_data.get("claimReview", [])
|
|
616
|
+
for review in claim_review:
|
|
617
|
+
rating = review.get("textualRating", "")
|
|
618
|
+
publisher = review.get("publisher", {}).get("name", "Unknown")
|
|
619
|
+
url = review.get("url", "")
|
|
620
|
+
|
|
621
|
+
# 判断评级
|
|
622
|
+
supports = self._parse_rating(rating)
|
|
623
|
+
|
|
624
|
+
evidences.append(Evidence(
|
|
625
|
+
source=f"Fact Check: {publisher}",
|
|
626
|
+
url=url,
|
|
627
|
+
snippet=f"Rating: {rating}",
|
|
628
|
+
relevance=0.9,
|
|
629
|
+
supports_claim=supports
|
|
630
|
+
))
|
|
631
|
+
|
|
632
|
+
if not evidences:
|
|
633
|
+
return ExternalValidationResult(
|
|
634
|
+
claim=claim,
|
|
635
|
+
status=ExternalValidationStatus.UNCERTAIN,
|
|
636
|
+
confidence=0.3,
|
|
637
|
+
summary="未找到相关的事实核查报告"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# 计算结论
|
|
641
|
+
status, confidence, summary = self._compute_verdict_from_checks(evidences)
|
|
642
|
+
|
|
643
|
+
return ExternalValidationResult(
|
|
644
|
+
claim=claim,
|
|
645
|
+
status=status,
|
|
646
|
+
confidence=confidence,
|
|
647
|
+
evidences=evidences,
|
|
648
|
+
summary=summary,
|
|
649
|
+
raw_response=data
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
return ExternalValidationResult(
|
|
654
|
+
claim=claim,
|
|
655
|
+
status=ExternalValidationStatus.ERROR,
|
|
656
|
+
confidence=0.0,
|
|
657
|
+
error=str(e)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def _parse_rating(self, rating: str) -> Optional[bool]:
|
|
661
|
+
"""解析事实核查评级"""
|
|
662
|
+
rating_lower = rating.lower()
|
|
663
|
+
|
|
664
|
+
# 真实
|
|
665
|
+
true_patterns = ["true", "correct", "accurate", "真实", "正确"]
|
|
666
|
+
for pattern in true_patterns:
|
|
667
|
+
if pattern in rating_lower:
|
|
668
|
+
return True
|
|
669
|
+
|
|
670
|
+
# 虚假
|
|
671
|
+
false_patterns = ["false", "fake", "incorrect", "pants on fire", "虚假", "错误", "谣言"]
|
|
672
|
+
for pattern in false_patterns:
|
|
673
|
+
if pattern in rating_lower:
|
|
674
|
+
return False
|
|
675
|
+
|
|
676
|
+
return None
|
|
677
|
+
|
|
678
|
+
def _compute_verdict_from_checks(
|
|
679
|
+
self,
|
|
680
|
+
evidences: List[Evidence]
|
|
681
|
+
) -> tuple[ExternalValidationStatus, float, str]:
|
|
682
|
+
"""从事实核查计算结论"""
|
|
683
|
+
verified = sum(1 for e in evidences if e.supports_claim is True)
|
|
684
|
+
refuted = sum(1 for e in evidences if e.supports_claim is False)
|
|
685
|
+
uncertain = len(evidences) - verified - refuted
|
|
686
|
+
|
|
687
|
+
if verified > refuted and verified > uncertain:
|
|
688
|
+
return (
|
|
689
|
+
ExternalValidationStatus.VERIFIED,
|
|
690
|
+
0.7 + 0.1 * min(verified, 3),
|
|
691
|
+
f"{verified} 个事实核查支持该声明"
|
|
692
|
+
)
|
|
693
|
+
elif refuted > verified:
|
|
694
|
+
return (
|
|
695
|
+
ExternalValidationStatus.REFUTED,
|
|
696
|
+
0.7 + 0.1 * min(refuted, 3),
|
|
697
|
+
f"{refuted} 个事实核查反驳该声明"
|
|
698
|
+
)
|
|
699
|
+
else:
|
|
700
|
+
return (
|
|
701
|
+
ExternalValidationStatus.UNCERTAIN,
|
|
702
|
+
0.5,
|
|
703
|
+
f"事实核查结果不一致:{verified} 支持,{refuted} 反驳,{uncertain} 不确定"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
# ============ 组合验证器 ============
|
|
708
|
+
|
|
709
|
+
class CompositeValidator(BaseValidator):
|
|
710
|
+
"""
|
|
711
|
+
组合验证器
|
|
712
|
+
|
|
713
|
+
组合多个验证器,综合评估结果。
|
|
714
|
+
"""
|
|
715
|
+
|
|
716
|
+
def __init__(
|
|
717
|
+
self,
|
|
718
|
+
validators: Optional[List[BaseValidator]] = None,
|
|
719
|
+
strategy: str = "majority"
|
|
720
|
+
):
|
|
721
|
+
"""
|
|
722
|
+
初始化组合验证器
|
|
723
|
+
|
|
724
|
+
参数:
|
|
725
|
+
validators: 验证器列表
|
|
726
|
+
strategy: 组合策略 ("majority", "weighted", "any")
|
|
727
|
+
"""
|
|
728
|
+
self.validators = validators or []
|
|
729
|
+
self.strategy = strategy
|
|
730
|
+
|
|
731
|
+
@property
|
|
732
|
+
def name(self) -> str:
|
|
733
|
+
return f"composite_{len(self.validators)}"
|
|
734
|
+
|
|
735
|
+
def add_validator(self, validator: BaseValidator):
|
|
736
|
+
"""添加验证器"""
|
|
737
|
+
self.validators.append(validator)
|
|
738
|
+
|
|
739
|
+
def is_available(self) -> bool:
|
|
740
|
+
return any(v.is_available() for v in self.validators)
|
|
741
|
+
|
|
742
|
+
async def validate_async(self, claim: str, context: Optional["CognitiveContext"] = None) -> ExternalValidationResult:
|
|
743
|
+
"""使用所有验证器验证声明"""
|
|
744
|
+
available_validators = [v for v in self.validators if v.is_available()]
|
|
745
|
+
|
|
746
|
+
if not available_validators:
|
|
747
|
+
return ExternalValidationResult(
|
|
748
|
+
claim=claim,
|
|
749
|
+
status=ExternalValidationStatus.UNAVAILABLE,
|
|
750
|
+
confidence=0.0,
|
|
751
|
+
error="没有可用的验证器"
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
# 并行执行所有验证器
|
|
755
|
+
tasks = [v.validate_async(claim, context) for v in available_validators]
|
|
756
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
757
|
+
|
|
758
|
+
# 收集结果
|
|
759
|
+
valid_results: List[tuple[str, ExternalValidationResult]] = []
|
|
760
|
+
for i, result in enumerate(results):
|
|
761
|
+
if isinstance(result, Exception):
|
|
762
|
+
continue
|
|
763
|
+
valid_results.append((available_validators[i].name, cast(ExternalValidationResult, result)))
|
|
764
|
+
|
|
765
|
+
if not valid_results:
|
|
766
|
+
return ExternalValidationResult(
|
|
767
|
+
claim=claim,
|
|
768
|
+
status=ExternalValidationStatus.ERROR,
|
|
769
|
+
confidence=0.0,
|
|
770
|
+
error="所有验证器都失败了"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# 根据策略组合结果
|
|
774
|
+
return self._combine_results(claim, valid_results)
|
|
775
|
+
|
|
776
|
+
def _combine_results(
|
|
777
|
+
self,
|
|
778
|
+
claim: str,
|
|
779
|
+
results: List[tuple[str, ExternalValidationResult]]
|
|
780
|
+
) -> ExternalValidationResult:
|
|
781
|
+
"""组合多个验证结果"""
|
|
782
|
+
all_evidences: List[Evidence] = []
|
|
783
|
+
|
|
784
|
+
# 统计各状态
|
|
785
|
+
status_counts = {
|
|
786
|
+
ExternalValidationStatus.VERIFIED: 0,
|
|
787
|
+
ExternalValidationStatus.REFUTED: 0,
|
|
788
|
+
ExternalValidationStatus.UNCERTAIN: 0,
|
|
789
|
+
ExternalValidationStatus.ERROR: 0
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
weighted_confidence = 0.0
|
|
793
|
+
total_weight = 0.0
|
|
794
|
+
|
|
795
|
+
for name, result in results:
|
|
796
|
+
status_counts[result.status] += 1
|
|
797
|
+
all_evidences.extend(result.evidences)
|
|
798
|
+
|
|
799
|
+
# 加权置信度
|
|
800
|
+
weight = 1.0 if result.status != ExternalValidationStatus.ERROR else 0.0
|
|
801
|
+
weighted_confidence += result.confidence * weight
|
|
802
|
+
total_weight += weight
|
|
803
|
+
|
|
804
|
+
# 根据策略确定最终状态
|
|
805
|
+
if self.strategy == "majority":
|
|
806
|
+
# 多数投票
|
|
807
|
+
max_count = 0
|
|
808
|
+
final_status = ExternalValidationStatus.UNCERTAIN
|
|
809
|
+
for status, count in status_counts.items():
|
|
810
|
+
if count > max_count and status != ExternalValidationStatus.ERROR:
|
|
811
|
+
max_count = count
|
|
812
|
+
final_status = status
|
|
813
|
+
elif self.strategy == "any":
|
|
814
|
+
# 只要有一个验证成功就返回
|
|
815
|
+
for status in [ExternalValidationStatus.VERIFIED, ExternalValidationStatus.REFUTED]:
|
|
816
|
+
if status_counts[status] > 0:
|
|
817
|
+
final_status = status
|
|
818
|
+
break
|
|
819
|
+
else:
|
|
820
|
+
final_status = ExternalValidationStatus.UNCERTAIN
|
|
821
|
+
else: # weighted
|
|
822
|
+
# 加权平均
|
|
823
|
+
verified_weight = status_counts[ExternalValidationStatus.VERIFIED]
|
|
824
|
+
refuted_weight = status_counts[ExternalValidationStatus.REFUTED]
|
|
825
|
+
|
|
826
|
+
if verified_weight > refuted_weight:
|
|
827
|
+
final_status = ExternalValidationStatus.VERIFIED
|
|
828
|
+
elif refuted_weight > verified_weight:
|
|
829
|
+
final_status = ExternalValidationStatus.REFUTED
|
|
830
|
+
else:
|
|
831
|
+
final_status = ExternalValidationStatus.UNCERTAIN
|
|
832
|
+
|
|
833
|
+
final_confidence = weighted_confidence / total_weight if total_weight > 0 else 0.0
|
|
834
|
+
|
|
835
|
+
# 生成摘要
|
|
836
|
+
summary_parts = []
|
|
837
|
+
for name, result in results:
|
|
838
|
+
if result.status != ExternalValidationStatus.ERROR:
|
|
839
|
+
summary_parts.append(f"{name}: {result.status.value}")
|
|
840
|
+
|
|
841
|
+
return ExternalValidationResult(
|
|
842
|
+
claim=claim,
|
|
843
|
+
status=final_status,
|
|
844
|
+
confidence=final_confidence,
|
|
845
|
+
evidences=all_evidences,
|
|
846
|
+
summary="; ".join(summary_parts)
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# ============ 便捷函数 ============
|
|
851
|
+
|
|
852
|
+
def create_default_validator(
|
|
853
|
+
duckduckgo: bool = True,
|
|
854
|
+
bing_api_key: Optional[str] = None,
|
|
855
|
+
wikidata: bool = True,
|
|
856
|
+
fact_check_api_key: Optional[str] = None
|
|
857
|
+
) -> CompositeValidator:
|
|
858
|
+
"""
|
|
859
|
+
创建默认的组合验证器
|
|
860
|
+
|
|
861
|
+
参数:
|
|
862
|
+
duckduckgo: 是否启用 DuckDuckGo 搜索
|
|
863
|
+
bing_api_key: Bing API 密钥
|
|
864
|
+
wikidata: 是否启用 Wikidata 知识图谱
|
|
865
|
+
fact_check_api_key: Google Fact Check API 密钥
|
|
866
|
+
返回:
|
|
867
|
+
配置好的组合验证器
|
|
868
|
+
"""
|
|
869
|
+
validators: List[BaseValidator] = []
|
|
870
|
+
|
|
871
|
+
if duckduckgo:
|
|
872
|
+
validators.append(WebSearchValidator(engine="duckduckgo"))
|
|
873
|
+
|
|
874
|
+
if bing_api_key:
|
|
875
|
+
validators.append(WebSearchValidator(engine="bing", api_key=bing_api_key))
|
|
876
|
+
|
|
877
|
+
if wikidata:
|
|
878
|
+
validators.append(KnowledgeGraphValidator())
|
|
879
|
+
|
|
880
|
+
if fact_check_api_key:
|
|
881
|
+
validators.append(FactCheckValidator(api_key=fact_check_api_key))
|
|
882
|
+
|
|
883
|
+
return CompositeValidator(validators=validators)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def verify_claim(
|
|
887
|
+
claim: str,
|
|
888
|
+
validators: Optional[List[str]] = None,
|
|
889
|
+
api_keys: Optional[Dict[str, str]] = None,
|
|
890
|
+
context: Optional["CognitiveContext"] = None
|
|
891
|
+
) -> ExternalValidationResult:
|
|
892
|
+
"""
|
|
893
|
+
验证声明的便捷函数
|
|
894
|
+
|
|
895
|
+
参数:
|
|
896
|
+
claim: 要验证的声明
|
|
897
|
+
validators: 验证器类型列表 (["web", "knowledge", "fact_check"])
|
|
898
|
+
api_keys: API 密钥字典 {"bing": "...", "fact_check": "..."}
|
|
899
|
+
context: 认知上下文
|
|
900
|
+
返回:
|
|
901
|
+
ExternalValidationResult 对象
|
|
902
|
+
"""
|
|
903
|
+
import os
|
|
904
|
+
|
|
905
|
+
api_keys = api_keys or {}
|
|
906
|
+
validators = validators or ["web", "knowledge"]
|
|
907
|
+
|
|
908
|
+
validator_list: List[BaseValidator] = []
|
|
909
|
+
|
|
910
|
+
if "web" in validators:
|
|
911
|
+
bing_key = api_keys.get("bing") or os.environ.get("BING_API_KEY")
|
|
912
|
+
if bing_key:
|
|
913
|
+
validator_list.append(WebSearchValidator(engine="bing", api_key=bing_key))
|
|
914
|
+
else:
|
|
915
|
+
validator_list.append(WebSearchValidator(engine="duckduckgo"))
|
|
916
|
+
|
|
917
|
+
if "knowledge" in validators:
|
|
918
|
+
validator_list.append(KnowledgeGraphValidator())
|
|
919
|
+
|
|
920
|
+
if "fact_check" in validators:
|
|
921
|
+
fc_key = api_keys.get("fact_check") or os.environ.get("GOOGLE_FACT_CHECK_API_KEY")
|
|
922
|
+
if fc_key:
|
|
923
|
+
validator_list.append(FactCheckValidator(api_key=fc_key))
|
|
924
|
+
|
|
925
|
+
composite = CompositeValidator(validators=validator_list)
|
|
926
|
+
return composite.validate(claim, context)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
async def verify_claim_async(
|
|
930
|
+
claim: str,
|
|
931
|
+
validators: Optional[List[str]] = None,
|
|
932
|
+
api_keys: Optional[Dict[str, str]] = None,
|
|
933
|
+
context: Optional["CognitiveContext"] = None
|
|
934
|
+
) -> ExternalValidationResult:
|
|
935
|
+
"""
|
|
936
|
+
异步验证声明的便捷函数
|
|
937
|
+
"""
|
|
938
|
+
import os
|
|
939
|
+
|
|
940
|
+
api_keys = api_keys or {}
|
|
941
|
+
validators = validators or ["web", "knowledge"]
|
|
942
|
+
|
|
943
|
+
validator_list: List[BaseValidator] = []
|
|
944
|
+
|
|
945
|
+
if "web" in validators:
|
|
946
|
+
bing_key = api_keys.get("bing") or os.environ.get("BING_API_KEY")
|
|
947
|
+
if bing_key:
|
|
948
|
+
validator_list.append(WebSearchValidator(engine="bing", api_key=bing_key))
|
|
949
|
+
else:
|
|
950
|
+
validator_list.append(WebSearchValidator(engine="duckduckgo"))
|
|
951
|
+
|
|
952
|
+
if "knowledge" in validators:
|
|
953
|
+
validator_list.append(KnowledgeGraphValidator())
|
|
954
|
+
|
|
955
|
+
if "fact_check" in validators:
|
|
956
|
+
fc_key = api_keys.get("fact_check") or os.environ.get("GOOGLE_FACT_CHECK_API_KEY")
|
|
957
|
+
if fc_key:
|
|
958
|
+
validator_list.append(FactCheckValidator(api_key=fc_key))
|
|
959
|
+
|
|
960
|
+
composite = CompositeValidator(validators=validator_list)
|
|
961
|
+
return await composite.validate_async(claim, context)
|