FactLite 1.0.0__tar.gz
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.
- factlite-1.0.0/FactLite/__init__.py +18 -0
- factlite-1.0.0/FactLite/core/actions.py +41 -0
- factlite-1.0.0/FactLite/core/config.py +12 -0
- factlite-1.0.0/FactLite/core/rules.py +124 -0
- factlite-1.0.0/FactLite/core/verify.py +148 -0
- factlite-1.0.0/FactLite.egg-info/PKG-INFO +210 -0
- factlite-1.0.0/FactLite.egg-info/SOURCES.txt +13 -0
- factlite-1.0.0/FactLite.egg-info/dependency_links.txt +1 -0
- factlite-1.0.0/FactLite.egg-info/requires.txt +1 -0
- factlite-1.0.0/FactLite.egg-info/top_level.txt +1 -0
- factlite-1.0.0/LICENSE +21 -0
- factlite-1.0.0/PKG-INFO +210 -0
- factlite-1.0.0/README.md +173 -0
- factlite-1.0.0/pyproject.toml +26 -0
- factlite-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .core.actions import ReturnBest, RaiseError, ReturnSafeMessage, FallbackAction
|
|
2
|
+
from .core.rules import BaseRule, LLMJudge, CustomJudge
|
|
3
|
+
from .core.verify import verify
|
|
4
|
+
|
|
5
|
+
# Export components
|
|
6
|
+
class Actions:
|
|
7
|
+
ReturnBest = ReturnBest
|
|
8
|
+
RaiseError = RaiseError
|
|
9
|
+
ReturnSafeMessage = ReturnSafeMessage
|
|
10
|
+
FallbackAction = FallbackAction
|
|
11
|
+
|
|
12
|
+
class rules:
|
|
13
|
+
BaseRule = BaseRule
|
|
14
|
+
LLMJudge = LLMJudge
|
|
15
|
+
CustomJudge = CustomJudge
|
|
16
|
+
|
|
17
|
+
action = Actions()
|
|
18
|
+
__all__ = ["verify", "rules", "action"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
class FallbackAction(ABC):
|
|
7
|
+
"""Base class for all fallback actions."""
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def execute(self, prompt: str, last_answer: str, feedback: str) -> str:
|
|
10
|
+
"""Execute the fallback action.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
prompt (str): The original user question
|
|
14
|
+
last_answer (str): The model's last generated answer
|
|
15
|
+
feedback (str): The feedback from the judge
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
str: The fallback action's response
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class ReturnBest(FallbackAction):
|
|
23
|
+
"""Return the last generated answer despite failing verification."""
|
|
24
|
+
def execute(self, prompt: str, last_answer: str, feedback: str) -> str:
|
|
25
|
+
logger.warning("Returning the last generated answer despite failing verification.")
|
|
26
|
+
return last_answer
|
|
27
|
+
|
|
28
|
+
class RaiseError(FallbackAction):
|
|
29
|
+
"""Raise an exception if the answer fails verification."""
|
|
30
|
+
def execute(self, prompt: str, last_answer: str, feedback: str) -> str:
|
|
31
|
+
logger.error("Raising exception due to factual verification failure.")
|
|
32
|
+
raise Exception(f"Answer failed factual verification. Last feedback: {feedback}")
|
|
33
|
+
|
|
34
|
+
class ReturnSafeMessage(FallbackAction):
|
|
35
|
+
"""Return a safe message if the answer fails verification."""
|
|
36
|
+
def __init__(self, safe_message="抱歉,AI 暂时无法针对该问题给出有确切把握的回答。"):
|
|
37
|
+
self.safe_message = safe_message
|
|
38
|
+
|
|
39
|
+
def execute(self, prompt: str, last_answer: str, feedback: str) -> str:
|
|
40
|
+
logger.warning(f"Returning safe message. Original hallucination feedback: {feedback}")
|
|
41
|
+
return self.safe_message
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .actions import ReturnBest, FallbackAction
|
|
2
|
+
from .rules import BaseRule
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
def __init__(self, rule: BaseRule = None,
|
|
6
|
+
max_retries: int = 2,
|
|
7
|
+
on_fail: FallbackAction = ReturnBest()):
|
|
8
|
+
"""Initialize the configuration for the FactLite framework"""
|
|
9
|
+
|
|
10
|
+
self.rule = rule
|
|
11
|
+
self.max_retries = max_retries
|
|
12
|
+
self.on_fail = on_fail
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import openai
|
|
3
|
+
import json
|
|
4
|
+
import inspect
|
|
5
|
+
|
|
6
|
+
class BaseRule(ABC):
|
|
7
|
+
"""Base rule class for all judge implementations"""
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def evaluate(self, user_prompt, answer):
|
|
10
|
+
"""Evaluate the answer against the user prompt
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
user_prompt (str): The original user question
|
|
14
|
+
answer (str): The model's answer
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
dict: A dictionary with "is_pass" (bool) and "feedback" (str) keys
|
|
18
|
+
"""
|
|
19
|
+
raise NotImplementedError("Subclasses must implement evaluate method")
|
|
20
|
+
|
|
21
|
+
class LLMJudge(BaseRule):
|
|
22
|
+
# Note: Assuming the use of the new OpenAI SDK (>=1.0.0)
|
|
23
|
+
def __init__(self, model="gpt-4o-mini", api_key=None, base_url=None):
|
|
24
|
+
self.model = model
|
|
25
|
+
self.base_url = base_url
|
|
26
|
+
api_key = api_key or openai.api_key
|
|
27
|
+
self.client = openai.OpenAI(api_key=api_key, base_url=base_url) if hasattr(openai, "OpenAI") else openai
|
|
28
|
+
|
|
29
|
+
def evaluate(self, user_prompt, answer):
|
|
30
|
+
evaluation_prompt = f"""You are a fact-checking judge. Evaluate the following response to determine if it accurately answers the user's question. Return a JSON object with two fields:
|
|
31
|
+
- is_pass: boolean indicating if the response is factually correct
|
|
32
|
+
- feedback: detailed criticism if is_pass is false, or empty string if true
|
|
33
|
+
|
|
34
|
+
User question: {user_prompt}
|
|
35
|
+
Response: {answer}
|
|
36
|
+
|
|
37
|
+
JSON output:"""
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
if hasattr(openai, "OpenAI"):
|
|
41
|
+
response = self.client.chat.completions.create(
|
|
42
|
+
model=self.model,
|
|
43
|
+
messages=[
|
|
44
|
+
{"role": "system", "content": "You are a fact-checking judge. Return only JSON output."},
|
|
45
|
+
{"role": "user", "content": evaluation_prompt}
|
|
46
|
+
],
|
|
47
|
+
response_format={"type": "json_object"}
|
|
48
|
+
)
|
|
49
|
+
result_str = response.choices[0].message.content
|
|
50
|
+
else:
|
|
51
|
+
response = self.client.ChatCompletion.create(
|
|
52
|
+
model=self.model,
|
|
53
|
+
messages=[
|
|
54
|
+
{"role": "system", "content": "You are a fact-checking judge. Return only JSON output."},
|
|
55
|
+
{"role": "user", "content": evaluation_prompt}
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
result_str = response.choices[0].message.content
|
|
59
|
+
|
|
60
|
+
return json.loads(result_str)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
error_message = f"LLMJudge API call failed: {str(e)}. Please check your API key and network connection."
|
|
63
|
+
return {
|
|
64
|
+
"is_pass": False,
|
|
65
|
+
"feedback": error_message
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class CustomJudge(BaseRule):
|
|
69
|
+
def __init__(self, eval_func):
|
|
70
|
+
"""Initialize CustomJudge with a custom evaluation function
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
eval_func (callable): A function that takes user_prompt and answer as parameters
|
|
74
|
+
and returns a dict with "is_pass" and "feedback" keys
|
|
75
|
+
"""
|
|
76
|
+
if not callable(eval_func):
|
|
77
|
+
raise TypeError("eval_func must be a callable")
|
|
78
|
+
|
|
79
|
+
# Check if the function accepts at least two parameters
|
|
80
|
+
sig = inspect.signature(eval_func)
|
|
81
|
+
params = sig.parameters.values()
|
|
82
|
+
has_var_args = any(p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in params)
|
|
83
|
+
if not has_var_args and len(params) < 2:
|
|
84
|
+
raise ValueError("eval_func must accept at least two parameters: user_prompt and answer")
|
|
85
|
+
|
|
86
|
+
self.eval_func = eval_func
|
|
87
|
+
|
|
88
|
+
def evaluate(self, user_prompt, answer):
|
|
89
|
+
"""Evaluate the answer using the custom function with error handling
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
user_prompt (str): The original user question
|
|
93
|
+
answer (str): The model's answer
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict: A dictionary with "is_pass" (bool) and "feedback" (str) keys
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
# Call the custom evaluation function
|
|
100
|
+
result = self.eval_func(user_prompt, answer)
|
|
101
|
+
|
|
102
|
+
# Validate the result
|
|
103
|
+
if not isinstance(result, dict):
|
|
104
|
+
raise ValueError("eval_func must return a dictionary")
|
|
105
|
+
|
|
106
|
+
if "is_pass" not in result:
|
|
107
|
+
raise ValueError("Returned dictionary must contain 'is_pass' key")
|
|
108
|
+
|
|
109
|
+
if "feedback" not in result:
|
|
110
|
+
raise ValueError("Returned dictionary must contain 'feedback' key")
|
|
111
|
+
|
|
112
|
+
if not isinstance(result["is_pass"], bool):
|
|
113
|
+
raise ValueError("'is_pass' must be a boolean")
|
|
114
|
+
|
|
115
|
+
if not isinstance(result["feedback"], str):
|
|
116
|
+
raise ValueError("'feedback' must be a string")
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
except Exception as e:
|
|
120
|
+
# Convert any error to a failed evaluation
|
|
121
|
+
return {
|
|
122
|
+
"is_pass": False,
|
|
123
|
+
"feedback": f"Error in custom judge: {str(e)}. Please fix your evaluation function."
|
|
124
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import openai
|
|
4
|
+
import logging
|
|
5
|
+
import asyncio
|
|
6
|
+
from .actions import ReturnBest
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - [FactLite] - %(message)s', datefmt='%H:%M:%S')
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
def _get_prompt_value(user_prompt, args, kwargs):
|
|
13
|
+
"""Get the prompt value from either kwargs or args"""
|
|
14
|
+
prompt_value = kwargs.get(user_prompt)
|
|
15
|
+
arg_index = None
|
|
16
|
+
if not prompt_value and args:
|
|
17
|
+
prompt_value = args[0]
|
|
18
|
+
arg_index = 0
|
|
19
|
+
return prompt_value, arg_index
|
|
20
|
+
|
|
21
|
+
def _update_prompt(current_prompt, arg_index, args, kwargs, user_prompt):
|
|
22
|
+
"""Update the prompt in either args or kwargs"""
|
|
23
|
+
new_args = list(args)
|
|
24
|
+
new_kwargs = kwargs.copy()
|
|
25
|
+
if arg_index is not None:
|
|
26
|
+
new_args[arg_index] = current_prompt
|
|
27
|
+
else:
|
|
28
|
+
new_kwargs[user_prompt] = current_prompt
|
|
29
|
+
return new_args, new_kwargs
|
|
30
|
+
|
|
31
|
+
def _generate_reflection_prompt(prompt_value, best_answer, feedback):
|
|
32
|
+
"""Generate reflection prompt"""
|
|
33
|
+
return f"""[System Prompt: You need to self-reflect and self-correct]
|
|
34
|
+
Original user question: {prompt_value}
|
|
35
|
+
Your previous answer: {best_answer}
|
|
36
|
+
Judge's feedback: {feedback}
|
|
37
|
+
Please take a deep breath, strictly correct the errors mentioned above, and provide the final perfect answer."""
|
|
38
|
+
|
|
39
|
+
def verify(user_prompt=None, rule=None, max_retries=2, on_fail=ReturnBest(), config=None):
|
|
40
|
+
# Handle config parameter
|
|
41
|
+
if config:
|
|
42
|
+
max_retries = config.max_retries
|
|
43
|
+
on_fail = config.on_fail
|
|
44
|
+
rule = config.rule
|
|
45
|
+
|
|
46
|
+
def decorator(func):
|
|
47
|
+
# Check if the function is async
|
|
48
|
+
is_async = inspect.iscoroutinefunction(func)
|
|
49
|
+
|
|
50
|
+
if is_async:
|
|
51
|
+
@functools.wraps(func)
|
|
52
|
+
async def async_wrapper(*args, **kwargs):
|
|
53
|
+
prompt_value, arg_index = _get_prompt_value(user_prompt, args, kwargs)
|
|
54
|
+
|
|
55
|
+
retry_count = 0
|
|
56
|
+
best_answer = None
|
|
57
|
+
current_prompt = prompt_value
|
|
58
|
+
|
|
59
|
+
while retry_count <= max_retries:
|
|
60
|
+
if retry_count == 0:
|
|
61
|
+
logger.info("Generating initial answer...")
|
|
62
|
+
else:
|
|
63
|
+
logger.warning(f"Triggering reflection and rewrite, attempt {retry_count}...")
|
|
64
|
+
|
|
65
|
+
new_args, new_kwargs = _update_prompt(current_prompt, arg_index, args, kwargs, user_prompt)
|
|
66
|
+
answer = await func(*new_args, **new_kwargs)
|
|
67
|
+
best_answer = answer
|
|
68
|
+
|
|
69
|
+
logger.info("Evaluating answer quality...")
|
|
70
|
+
evaluation_result = await asyncio.to_thread(rule.evaluate, prompt_value, answer)
|
|
71
|
+
is_pass = evaluation_result.get("is_pass", False)
|
|
72
|
+
feedback = evaluation_result.get("feedback", "")
|
|
73
|
+
|
|
74
|
+
if is_pass:
|
|
75
|
+
if retry_count > 0:
|
|
76
|
+
logger.info("✅ Correction successful, returning the verified answer!")
|
|
77
|
+
else:
|
|
78
|
+
logger.info("✅ Initial draft is flawless, passing through!")
|
|
79
|
+
return answer
|
|
80
|
+
|
|
81
|
+
logger.error(f"❌ Hallucination or error detected: {feedback}")
|
|
82
|
+
|
|
83
|
+
current_prompt = _generate_reflection_prompt(prompt_value, best_answer, feedback)
|
|
84
|
+
retry_count += 1
|
|
85
|
+
|
|
86
|
+
logger.warning("Maximum retries reached, executing fallback strategy.")
|
|
87
|
+
action_instance = on_fail() if isinstance(on_fail, type) else on_fail
|
|
88
|
+
if inspect.iscoroutinefunction(action_instance.execute):
|
|
89
|
+
return await action_instance.execute(
|
|
90
|
+
prompt=prompt_value,
|
|
91
|
+
last_answer=best_answer,
|
|
92
|
+
feedback=feedback
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
return action_instance.execute(
|
|
96
|
+
prompt=prompt_value,
|
|
97
|
+
last_answer=best_answer,
|
|
98
|
+
feedback=feedback
|
|
99
|
+
)
|
|
100
|
+
return async_wrapper
|
|
101
|
+
else:
|
|
102
|
+
@functools.wraps(func)
|
|
103
|
+
def sync_wrapper(*args, **kwargs):
|
|
104
|
+
prompt_value, arg_index = _get_prompt_value(user_prompt, args, kwargs)
|
|
105
|
+
|
|
106
|
+
retry_count = 0
|
|
107
|
+
best_answer = None
|
|
108
|
+
current_prompt = prompt_value
|
|
109
|
+
|
|
110
|
+
while retry_count <= max_retries:
|
|
111
|
+
if retry_count == 0:
|
|
112
|
+
logger.info("Generating initial answer...")
|
|
113
|
+
else:
|
|
114
|
+
logger.warning(f"Triggering reflection and rewrite, attempt {retry_count}...")
|
|
115
|
+
|
|
116
|
+
new_args, new_kwargs = _update_prompt(current_prompt, arg_index, args, kwargs, user_prompt)
|
|
117
|
+
answer = func(*new_args, **new_kwargs)
|
|
118
|
+
best_answer = answer
|
|
119
|
+
|
|
120
|
+
logger.info("Evaluating answer quality...")
|
|
121
|
+
evaluation_result = rule.evaluate(prompt_value, answer)
|
|
122
|
+
is_pass = evaluation_result.get("is_pass", False)
|
|
123
|
+
feedback = evaluation_result.get("feedback", "")
|
|
124
|
+
|
|
125
|
+
if is_pass:
|
|
126
|
+
if retry_count > 0:
|
|
127
|
+
logger.info("✅ Correction successful, returning the verified answer!")
|
|
128
|
+
else:
|
|
129
|
+
logger.info("✅ Initial draft is flawless, passing through!")
|
|
130
|
+
return answer
|
|
131
|
+
|
|
132
|
+
logger.error(f"❌ Hallucination or error detected: {feedback}")
|
|
133
|
+
|
|
134
|
+
current_prompt = _generate_reflection_prompt(prompt_value, best_answer, feedback)
|
|
135
|
+
retry_count += 1
|
|
136
|
+
|
|
137
|
+
logger.warning("Maximum retries reached, executing fallback strategy.")
|
|
138
|
+
action_instance = on_fail() if isinstance(on_fail, type) else on_fail
|
|
139
|
+
return action_instance.execute(
|
|
140
|
+
prompt=prompt_value,
|
|
141
|
+
last_answer=best_answer,
|
|
142
|
+
feedback=feedback
|
|
143
|
+
)
|
|
144
|
+
return sync_wrapper
|
|
145
|
+
return decorator
|
|
146
|
+
|
|
147
|
+
# Add config method to verify function
|
|
148
|
+
verify.config = Config
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: FactLite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight framework for fact-checking AI-generated content
|
|
5
|
+
Author-email: SR思锐 团队 <srinternet@qq.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 SR思锐 团队
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/SRInternet-Studio/FactLite
|
|
28
|
+
Project-URL: Issues, https://github.com/SRInternet-Studio/FactLite/issues
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: openai>=1.0.0
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# FactLite 🪶
|
|
39
|
+
|
|
40
|
+
English | [中文](README_CN.md)
|
|
41
|
+
|
|
42
|
+
**Give Your LLM a "System 2" Brain with a Single Decorator.**
|
|
43
|
+
|
|
44
|
+
[](https://badge.fury.io/py/factlite)
|
|
45
|
+
[](https://travis-ci.org/your-username/factlite)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
[](https://www.python.org/downloads/release/python-390/)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
In the last mile of deploying Generative AI, **hallucination is the final boss**. Heavy frameworks like LangChain introduce too much boilerplate and complexity, while raw API calls offer no safety net.
|
|
52
|
+
|
|
53
|
+
**FactLite** is a production-ready, feather-light Python micro-framework designed to solve this exact problem. It enhances your existing LLM calls with an automated, self-correcting evaluation loop, inspired by the top-tier **Agentic "Reflexion" Architecture**, without forcing you to refactor your codebase.
|
|
54
|
+
|
|
55
|
+
## 🚀 Key Features
|
|
56
|
+
|
|
57
|
+
* **✨ Zero-Intrusion:** Add fact-checking and self-correction to any function with a single `@verify` decorator. No need to rewrite your existing logic.
|
|
58
|
+
* **⚡️ Async-Native & Concurrency Safe:** Built from the ground up to support `async/await`. The evaluation process runs in a separate thread to prevent blocking your main event loop, making it perfect for high-performance web backends like FastAPI.
|
|
59
|
+
* **🤖 Agentic Workflow:** Implements an automated **Generate -> Evaluate -> Reflect** loop. Your LLM is forced to critique and iteratively improve its own answers until they meet your quality standards.
|
|
60
|
+
* **🧩 Extensible & Pluggable:**
|
|
61
|
+
* Bring your own judge! Use the built-in `LLMJudge` or create your own validation logic (e.g., regex, database lookups, type checks) with `CustomJudge`.
|
|
62
|
+
* Define your own failure policies. Raise an error, return a safe message, or trigger a webhook with custom `FallbackAction`.
|
|
63
|
+
* **🌐 Framework Agnostic:** FactLite doesn't care how you call your LLM. Whether you're using the `openai` SDK, `anthropic`'s client, or a simple `requests.post` call to a local model, as long as it's a Python function that returns a string, FactLite can safeguard it.
|
|
64
|
+
|
|
65
|
+
## 📦 Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install factlite
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 🎯 Quick Start: The "Aha!" Moment
|
|
72
|
+
|
|
73
|
+
See how easy it is to upgrade your existing code from a simple API call to a self-correcting agent.
|
|
74
|
+
|
|
75
|
+
**Before: A standard, unprotected LLM call.**
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import openai
|
|
79
|
+
|
|
80
|
+
client = openai.OpenAI(api_key="your-key")
|
|
81
|
+
|
|
82
|
+
def ask_ai(question: str):
|
|
83
|
+
response = client.chat.completions.create(
|
|
84
|
+
model="gpt-3.5-turbo",
|
|
85
|
+
messages=[{"role": "user", "content": question}]
|
|
86
|
+
)
|
|
87
|
+
return response.choices[0].message.content
|
|
88
|
+
|
|
89
|
+
# This might return a factually incorrect answer, and you'd never know.
|
|
90
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**After: Protected by FactLite with a single line of code.**
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import openai
|
|
97
|
+
from FactLite import verify, rules, action
|
|
98
|
+
|
|
99
|
+
client = openai.OpenAI(api_key="your-key")
|
|
100
|
+
|
|
101
|
+
# Configure a powerful judge and your API key
|
|
102
|
+
config = verify.config(
|
|
103
|
+
api_key="your-key",
|
|
104
|
+
rule=rules.LLMJudge(model="gpt-4o-mini"),
|
|
105
|
+
max_retries=1
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@verify(config=config, user_prompt="question") # Just add this decorator!
|
|
109
|
+
def ask_ai(question: str):
|
|
110
|
+
response = client.chat.completions.create(
|
|
111
|
+
model="gpt-3.5-turbo",
|
|
112
|
+
messages=[{"role": "user", "content": question}]
|
|
113
|
+
)
|
|
114
|
+
return response.choices[0].message.content
|
|
115
|
+
|
|
116
|
+
# Now, the function will automatically correct itself before returning.
|
|
117
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**What you'll see in your console:**
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
10:30:05 - [FactLite] - Generating initial answer...
|
|
124
|
+
10:30:08 - [FactLite] - Evaluating answer quality...
|
|
125
|
+
10:30:12 - [FactLite] - ❌ Hallucination or error detected: The answer incorrectly states that Li Bai was related to the Song Dynasty. He was a poet from the Tang Dynasty.
|
|
126
|
+
10:30:12 - [FactLite] - Triggering reflection and rewrite, attempt 1...
|
|
127
|
+
10:30:16 - [FactLite] - Evaluating answer quality...
|
|
128
|
+
10:30:19 - [FactLite] - ✅ Correction successful, returning the verified answer!
|
|
129
|
+
|
|
130
|
+
No, Li Bai was not an emperor in the Song Dynasty. He was a renowned poet who lived during the Tang Dynasty (701-762 AD).
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 💡 Advanced Usage
|
|
134
|
+
|
|
135
|
+
### Async Support
|
|
136
|
+
|
|
137
|
+
FactLite automatically detects and supports `async` functions.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from openai import AsyncOpenAI
|
|
141
|
+
|
|
142
|
+
async_client = AsyncOpenAI(api_key="your-key")
|
|
143
|
+
|
|
144
|
+
@verify(config=config, user_prompt="question")
|
|
145
|
+
async def ask_ai_async(question: str):
|
|
146
|
+
response = await async_client.chat.completions.create(...)
|
|
147
|
+
return response.choices[0].message.content
|
|
148
|
+
|
|
149
|
+
# Run it
|
|
150
|
+
import asyncio
|
|
151
|
+
asyncio.run(ask_ai_async("Tell me about the Tang Dynasty."))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom Rules (`CustomJudge`)
|
|
155
|
+
|
|
156
|
+
Go beyond LLM-based checks. Enforce any local business logic you can imagine.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
def company_policy_judge(prompt, answer):
|
|
160
|
+
# Rule 1: No short answers
|
|
161
|
+
if len(answer) < 50:
|
|
162
|
+
return {"is_pass": False, "feedback": "Answer is too short. Please be more detailed."}
|
|
163
|
+
# Rule 2: Don't mention competitors
|
|
164
|
+
if "Google" in answer:
|
|
165
|
+
return {"is_pass": False, "feedback": "Do not mention competitor names."}
|
|
166
|
+
return {"is_pass": True, "feedback": ""}
|
|
167
|
+
|
|
168
|
+
@verify(rule=rules.CustomJudge(eval_func=company_policy_judge), user_prompt="prompt")
|
|
169
|
+
def ask_support_bot(prompt: str):
|
|
170
|
+
# ... your LLM call
|
|
171
|
+
pass
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Failure Actions (`FallbackAction`)
|
|
175
|
+
|
|
176
|
+
Decide exactly what happens when an answer fails all retries.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from FactLite import action
|
|
180
|
+
|
|
181
|
+
@verify(
|
|
182
|
+
...,
|
|
183
|
+
on_fail=action.ReturnSafeMessage("I'm sorry, I cannot provide a confident answer to that question at the moment.")
|
|
184
|
+
)
|
|
185
|
+
def ask_sensitive_question(...):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@verify(..., on_fail=action.RaiseError())
|
|
189
|
+
def ask_critical_question(...):
|
|
190
|
+
pass
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 🛠️ How It Works
|
|
194
|
+
|
|
195
|
+
FactLite's `@verify` decorator wraps your function in a simple yet powerful control loop:
|
|
196
|
+
|
|
197
|
+
1. **Generate**: Your original function is called to produce an initial draft.
|
|
198
|
+
2. **Evaluate**: The configured `rule` (e.g., `LLMJudge`) is invoked to assess the draft.
|
|
199
|
+
3. **Reflect & Retry**:
|
|
200
|
+
* If the evaluation passes, the answer is returned to the user.
|
|
201
|
+
* If it fails, the feedback is combined with the original prompt to create a "reflection prompt," forcing the LLM to correct its mistake. The process repeats from Step 1 until `max_retries` is reached.
|
|
202
|
+
4. **Fallback**: If all retries fail, the configured `on_fail` action is executed.
|
|
203
|
+
|
|
204
|
+
## 🤝 Contributing
|
|
205
|
+
|
|
206
|
+
Contributions are welcome! Whether it's a new rule, a new fallback action, or a performance improvement, feel free to open an issue or submit a pull request.
|
|
207
|
+
|
|
208
|
+
## 📄 License
|
|
209
|
+
|
|
210
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
FactLite/__init__.py
|
|
5
|
+
FactLite.egg-info/PKG-INFO
|
|
6
|
+
FactLite.egg-info/SOURCES.txt
|
|
7
|
+
FactLite.egg-info/dependency_links.txt
|
|
8
|
+
FactLite.egg-info/requires.txt
|
|
9
|
+
FactLite.egg-info/top_level.txt
|
|
10
|
+
FactLite/core/actions.py
|
|
11
|
+
FactLite/core/config.py
|
|
12
|
+
FactLite/core/rules.py
|
|
13
|
+
FactLite/core/verify.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openai>=1.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
FactLite
|
factlite-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SR思锐 团队
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
factlite-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: FactLite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight framework for fact-checking AI-generated content
|
|
5
|
+
Author-email: SR思锐 团队 <srinternet@qq.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 SR思锐 团队
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
Project-URL: Homepage, https://github.com/SRInternet-Studio/FactLite
|
|
28
|
+
Project-URL: Issues, https://github.com/SRInternet-Studio/FactLite/issues
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: OS Independent
|
|
32
|
+
Requires-Python: >=3.9
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Requires-Dist: openai>=1.0.0
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# FactLite 🪶
|
|
39
|
+
|
|
40
|
+
English | [中文](README_CN.md)
|
|
41
|
+
|
|
42
|
+
**Give Your LLM a "System 2" Brain with a Single Decorator.**
|
|
43
|
+
|
|
44
|
+
[](https://badge.fury.io/py/factlite)
|
|
45
|
+
[](https://travis-ci.org/your-username/factlite)
|
|
46
|
+
[](https://opensource.org/licenses/MIT)
|
|
47
|
+
[](https://www.python.org/downloads/release/python-390/)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
In the last mile of deploying Generative AI, **hallucination is the final boss**. Heavy frameworks like LangChain introduce too much boilerplate and complexity, while raw API calls offer no safety net.
|
|
52
|
+
|
|
53
|
+
**FactLite** is a production-ready, feather-light Python micro-framework designed to solve this exact problem. It enhances your existing LLM calls with an automated, self-correcting evaluation loop, inspired by the top-tier **Agentic "Reflexion" Architecture**, without forcing you to refactor your codebase.
|
|
54
|
+
|
|
55
|
+
## 🚀 Key Features
|
|
56
|
+
|
|
57
|
+
* **✨ Zero-Intrusion:** Add fact-checking and self-correction to any function with a single `@verify` decorator. No need to rewrite your existing logic.
|
|
58
|
+
* **⚡️ Async-Native & Concurrency Safe:** Built from the ground up to support `async/await`. The evaluation process runs in a separate thread to prevent blocking your main event loop, making it perfect for high-performance web backends like FastAPI.
|
|
59
|
+
* **🤖 Agentic Workflow:** Implements an automated **Generate -> Evaluate -> Reflect** loop. Your LLM is forced to critique and iteratively improve its own answers until they meet your quality standards.
|
|
60
|
+
* **🧩 Extensible & Pluggable:**
|
|
61
|
+
* Bring your own judge! Use the built-in `LLMJudge` or create your own validation logic (e.g., regex, database lookups, type checks) with `CustomJudge`.
|
|
62
|
+
* Define your own failure policies. Raise an error, return a safe message, or trigger a webhook with custom `FallbackAction`.
|
|
63
|
+
* **🌐 Framework Agnostic:** FactLite doesn't care how you call your LLM. Whether you're using the `openai` SDK, `anthropic`'s client, or a simple `requests.post` call to a local model, as long as it's a Python function that returns a string, FactLite can safeguard it.
|
|
64
|
+
|
|
65
|
+
## 📦 Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install factlite
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 🎯 Quick Start: The "Aha!" Moment
|
|
72
|
+
|
|
73
|
+
See how easy it is to upgrade your existing code from a simple API call to a self-correcting agent.
|
|
74
|
+
|
|
75
|
+
**Before: A standard, unprotected LLM call.**
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import openai
|
|
79
|
+
|
|
80
|
+
client = openai.OpenAI(api_key="your-key")
|
|
81
|
+
|
|
82
|
+
def ask_ai(question: str):
|
|
83
|
+
response = client.chat.completions.create(
|
|
84
|
+
model="gpt-3.5-turbo",
|
|
85
|
+
messages=[{"role": "user", "content": question}]
|
|
86
|
+
)
|
|
87
|
+
return response.choices[0].message.content
|
|
88
|
+
|
|
89
|
+
# This might return a factually incorrect answer, and you'd never know.
|
|
90
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**After: Protected by FactLite with a single line of code.**
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import openai
|
|
97
|
+
from FactLite import verify, rules, action
|
|
98
|
+
|
|
99
|
+
client = openai.OpenAI(api_key="your-key")
|
|
100
|
+
|
|
101
|
+
# Configure a powerful judge and your API key
|
|
102
|
+
config = verify.config(
|
|
103
|
+
api_key="your-key",
|
|
104
|
+
rule=rules.LLMJudge(model="gpt-4o-mini"),
|
|
105
|
+
max_retries=1
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@verify(config=config, user_prompt="question") # Just add this decorator!
|
|
109
|
+
def ask_ai(question: str):
|
|
110
|
+
response = client.chat.completions.create(
|
|
111
|
+
model="gpt-3.5-turbo",
|
|
112
|
+
messages=[{"role": "user", "content": question}]
|
|
113
|
+
)
|
|
114
|
+
return response.choices[0].message.content
|
|
115
|
+
|
|
116
|
+
# Now, the function will automatically correct itself before returning.
|
|
117
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**What you'll see in your console:**
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
10:30:05 - [FactLite] - Generating initial answer...
|
|
124
|
+
10:30:08 - [FactLite] - Evaluating answer quality...
|
|
125
|
+
10:30:12 - [FactLite] - ❌ Hallucination or error detected: The answer incorrectly states that Li Bai was related to the Song Dynasty. He was a poet from the Tang Dynasty.
|
|
126
|
+
10:30:12 - [FactLite] - Triggering reflection and rewrite, attempt 1...
|
|
127
|
+
10:30:16 - [FactLite] - Evaluating answer quality...
|
|
128
|
+
10:30:19 - [FactLite] - ✅ Correction successful, returning the verified answer!
|
|
129
|
+
|
|
130
|
+
No, Li Bai was not an emperor in the Song Dynasty. He was a renowned poet who lived during the Tang Dynasty (701-762 AD).
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 💡 Advanced Usage
|
|
134
|
+
|
|
135
|
+
### Async Support
|
|
136
|
+
|
|
137
|
+
FactLite automatically detects and supports `async` functions.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from openai import AsyncOpenAI
|
|
141
|
+
|
|
142
|
+
async_client = AsyncOpenAI(api_key="your-key")
|
|
143
|
+
|
|
144
|
+
@verify(config=config, user_prompt="question")
|
|
145
|
+
async def ask_ai_async(question: str):
|
|
146
|
+
response = await async_client.chat.completions.create(...)
|
|
147
|
+
return response.choices[0].message.content
|
|
148
|
+
|
|
149
|
+
# Run it
|
|
150
|
+
import asyncio
|
|
151
|
+
asyncio.run(ask_ai_async("Tell me about the Tang Dynasty."))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom Rules (`CustomJudge`)
|
|
155
|
+
|
|
156
|
+
Go beyond LLM-based checks. Enforce any local business logic you can imagine.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
def company_policy_judge(prompt, answer):
|
|
160
|
+
# Rule 1: No short answers
|
|
161
|
+
if len(answer) < 50:
|
|
162
|
+
return {"is_pass": False, "feedback": "Answer is too short. Please be more detailed."}
|
|
163
|
+
# Rule 2: Don't mention competitors
|
|
164
|
+
if "Google" in answer:
|
|
165
|
+
return {"is_pass": False, "feedback": "Do not mention competitor names."}
|
|
166
|
+
return {"is_pass": True, "feedback": ""}
|
|
167
|
+
|
|
168
|
+
@verify(rule=rules.CustomJudge(eval_func=company_policy_judge), user_prompt="prompt")
|
|
169
|
+
def ask_support_bot(prompt: str):
|
|
170
|
+
# ... your LLM call
|
|
171
|
+
pass
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Failure Actions (`FallbackAction`)
|
|
175
|
+
|
|
176
|
+
Decide exactly what happens when an answer fails all retries.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from FactLite import action
|
|
180
|
+
|
|
181
|
+
@verify(
|
|
182
|
+
...,
|
|
183
|
+
on_fail=action.ReturnSafeMessage("I'm sorry, I cannot provide a confident answer to that question at the moment.")
|
|
184
|
+
)
|
|
185
|
+
def ask_sensitive_question(...):
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
@verify(..., on_fail=action.RaiseError())
|
|
189
|
+
def ask_critical_question(...):
|
|
190
|
+
pass
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## 🛠️ How It Works
|
|
194
|
+
|
|
195
|
+
FactLite's `@verify` decorator wraps your function in a simple yet powerful control loop:
|
|
196
|
+
|
|
197
|
+
1. **Generate**: Your original function is called to produce an initial draft.
|
|
198
|
+
2. **Evaluate**: The configured `rule` (e.g., `LLMJudge`) is invoked to assess the draft.
|
|
199
|
+
3. **Reflect & Retry**:
|
|
200
|
+
* If the evaluation passes, the answer is returned to the user.
|
|
201
|
+
* If it fails, the feedback is combined with the original prompt to create a "reflection prompt," forcing the LLM to correct its mistake. The process repeats from Step 1 until `max_retries` is reached.
|
|
202
|
+
4. **Fallback**: If all retries fail, the configured `on_fail` action is executed.
|
|
203
|
+
|
|
204
|
+
## 🤝 Contributing
|
|
205
|
+
|
|
206
|
+
Contributions are welcome! Whether it's a new rule, a new fallback action, or a performance improvement, feel free to open an issue or submit a pull request.
|
|
207
|
+
|
|
208
|
+
## 📄 License
|
|
209
|
+
|
|
210
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
factlite-1.0.0/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# FactLite 🪶
|
|
2
|
+
|
|
3
|
+
English | [中文](README_CN.md)
|
|
4
|
+
|
|
5
|
+
**Give Your LLM a "System 2" Brain with a Single Decorator.**
|
|
6
|
+
|
|
7
|
+
[](https://badge.fury.io/py/factlite)
|
|
8
|
+
[](https://travis-ci.org/your-username/factlite)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://www.python.org/downloads/release/python-390/)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
In the last mile of deploying Generative AI, **hallucination is the final boss**. Heavy frameworks like LangChain introduce too much boilerplate and complexity, while raw API calls offer no safety net.
|
|
15
|
+
|
|
16
|
+
**FactLite** is a production-ready, feather-light Python micro-framework designed to solve this exact problem. It enhances your existing LLM calls with an automated, self-correcting evaluation loop, inspired by the top-tier **Agentic "Reflexion" Architecture**, without forcing you to refactor your codebase.
|
|
17
|
+
|
|
18
|
+
## 🚀 Key Features
|
|
19
|
+
|
|
20
|
+
* **✨ Zero-Intrusion:** Add fact-checking and self-correction to any function with a single `@verify` decorator. No need to rewrite your existing logic.
|
|
21
|
+
* **⚡️ Async-Native & Concurrency Safe:** Built from the ground up to support `async/await`. The evaluation process runs in a separate thread to prevent blocking your main event loop, making it perfect for high-performance web backends like FastAPI.
|
|
22
|
+
* **🤖 Agentic Workflow:** Implements an automated **Generate -> Evaluate -> Reflect** loop. Your LLM is forced to critique and iteratively improve its own answers until they meet your quality standards.
|
|
23
|
+
* **🧩 Extensible & Pluggable:**
|
|
24
|
+
* Bring your own judge! Use the built-in `LLMJudge` or create your own validation logic (e.g., regex, database lookups, type checks) with `CustomJudge`.
|
|
25
|
+
* Define your own failure policies. Raise an error, return a safe message, or trigger a webhook with custom `FallbackAction`.
|
|
26
|
+
* **🌐 Framework Agnostic:** FactLite doesn't care how you call your LLM. Whether you're using the `openai` SDK, `anthropic`'s client, or a simple `requests.post` call to a local model, as long as it's a Python function that returns a string, FactLite can safeguard it.
|
|
27
|
+
|
|
28
|
+
## 📦 Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install factlite
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 🎯 Quick Start: The "Aha!" Moment
|
|
35
|
+
|
|
36
|
+
See how easy it is to upgrade your existing code from a simple API call to a self-correcting agent.
|
|
37
|
+
|
|
38
|
+
**Before: A standard, unprotected LLM call.**
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import openai
|
|
42
|
+
|
|
43
|
+
client = openai.OpenAI(api_key="your-key")
|
|
44
|
+
|
|
45
|
+
def ask_ai(question: str):
|
|
46
|
+
response = client.chat.completions.create(
|
|
47
|
+
model="gpt-3.5-turbo",
|
|
48
|
+
messages=[{"role": "user", "content": question}]
|
|
49
|
+
)
|
|
50
|
+
return response.choices[0].message.content
|
|
51
|
+
|
|
52
|
+
# This might return a factually incorrect answer, and you'd never know.
|
|
53
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**After: Protected by FactLite with a single line of code.**
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import openai
|
|
60
|
+
from FactLite import verify, rules, action
|
|
61
|
+
|
|
62
|
+
client = openai.OpenAI(api_key="your-key")
|
|
63
|
+
|
|
64
|
+
# Configure a powerful judge and your API key
|
|
65
|
+
config = verify.config(
|
|
66
|
+
api_key="your-key",
|
|
67
|
+
rule=rules.LLMJudge(model="gpt-4o-mini"),
|
|
68
|
+
max_retries=1
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@verify(config=config, user_prompt="question") # Just add this decorator!
|
|
72
|
+
def ask_ai(question: str):
|
|
73
|
+
response = client.chat.completions.create(
|
|
74
|
+
model="gpt-3.5-turbo",
|
|
75
|
+
messages=[{"role": "user", "content": question}]
|
|
76
|
+
)
|
|
77
|
+
return response.choices[0].message.content
|
|
78
|
+
|
|
79
|
+
# Now, the function will automatically correct itself before returning.
|
|
80
|
+
print(ask_ai("Was Li Bai an emperor in the Song Dynasty?"))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**What you'll see in your console:**
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
10:30:05 - [FactLite] - Generating initial answer...
|
|
87
|
+
10:30:08 - [FactLite] - Evaluating answer quality...
|
|
88
|
+
10:30:12 - [FactLite] - ❌ Hallucination or error detected: The answer incorrectly states that Li Bai was related to the Song Dynasty. He was a poet from the Tang Dynasty.
|
|
89
|
+
10:30:12 - [FactLite] - Triggering reflection and rewrite, attempt 1...
|
|
90
|
+
10:30:16 - [FactLite] - Evaluating answer quality...
|
|
91
|
+
10:30:19 - [FactLite] - ✅ Correction successful, returning the verified answer!
|
|
92
|
+
|
|
93
|
+
No, Li Bai was not an emperor in the Song Dynasty. He was a renowned poet who lived during the Tang Dynasty (701-762 AD).
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 💡 Advanced Usage
|
|
97
|
+
|
|
98
|
+
### Async Support
|
|
99
|
+
|
|
100
|
+
FactLite automatically detects and supports `async` functions.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from openai import AsyncOpenAI
|
|
104
|
+
|
|
105
|
+
async_client = AsyncOpenAI(api_key="your-key")
|
|
106
|
+
|
|
107
|
+
@verify(config=config, user_prompt="question")
|
|
108
|
+
async def ask_ai_async(question: str):
|
|
109
|
+
response = await async_client.chat.completions.create(...)
|
|
110
|
+
return response.choices[0].message.content
|
|
111
|
+
|
|
112
|
+
# Run it
|
|
113
|
+
import asyncio
|
|
114
|
+
asyncio.run(ask_ai_async("Tell me about the Tang Dynasty."))
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Custom Rules (`CustomJudge`)
|
|
118
|
+
|
|
119
|
+
Go beyond LLM-based checks. Enforce any local business logic you can imagine.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
def company_policy_judge(prompt, answer):
|
|
123
|
+
# Rule 1: No short answers
|
|
124
|
+
if len(answer) < 50:
|
|
125
|
+
return {"is_pass": False, "feedback": "Answer is too short. Please be more detailed."}
|
|
126
|
+
# Rule 2: Don't mention competitors
|
|
127
|
+
if "Google" in answer:
|
|
128
|
+
return {"is_pass": False, "feedback": "Do not mention competitor names."}
|
|
129
|
+
return {"is_pass": True, "feedback": ""}
|
|
130
|
+
|
|
131
|
+
@verify(rule=rules.CustomJudge(eval_func=company_policy_judge), user_prompt="prompt")
|
|
132
|
+
def ask_support_bot(prompt: str):
|
|
133
|
+
# ... your LLM call
|
|
134
|
+
pass
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Failure Actions (`FallbackAction`)
|
|
138
|
+
|
|
139
|
+
Decide exactly what happens when an answer fails all retries.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from FactLite import action
|
|
143
|
+
|
|
144
|
+
@verify(
|
|
145
|
+
...,
|
|
146
|
+
on_fail=action.ReturnSafeMessage("I'm sorry, I cannot provide a confident answer to that question at the moment.")
|
|
147
|
+
)
|
|
148
|
+
def ask_sensitive_question(...):
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
@verify(..., on_fail=action.RaiseError())
|
|
152
|
+
def ask_critical_question(...):
|
|
153
|
+
pass
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 🛠️ How It Works
|
|
157
|
+
|
|
158
|
+
FactLite's `@verify` decorator wraps your function in a simple yet powerful control loop:
|
|
159
|
+
|
|
160
|
+
1. **Generate**: Your original function is called to produce an initial draft.
|
|
161
|
+
2. **Evaluate**: The configured `rule` (e.g., `LLMJudge`) is invoked to assess the draft.
|
|
162
|
+
3. **Reflect & Retry**:
|
|
163
|
+
* If the evaluation passes, the answer is returned to the user.
|
|
164
|
+
* If it fails, the feedback is combined with the original prompt to create a "reflection prompt," forcing the LLM to correct its mistake. The process repeats from Step 1 until `max_retries` is reached.
|
|
165
|
+
4. **Fallback**: If all retries fail, the configured `on_fail` action is executed.
|
|
166
|
+
|
|
167
|
+
## 🤝 Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome! Whether it's a new rule, a new fallback action, or a performance improvement, feel free to open an issue or submit a pull request.
|
|
170
|
+
|
|
171
|
+
## 📄 License
|
|
172
|
+
|
|
173
|
+
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "FactLite"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A lightweight framework for fact-checking AI-generated content"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "SR思锐 团队", email = "srinternet@qq.com" },
|
|
12
|
+
]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.9"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"openai>=1.0.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
"Homepage" = "https://github.com/SRInternet-Studio/FactLite"
|
|
26
|
+
"Issues" = "https://github.com/SRInternet-Studio/FactLite/issues"
|
factlite-1.0.0/setup.cfg
ADDED