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.
@@ -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
+ [![PyPI version](https://badge.fury.io/py/factlite.svg)](https://badge.fury.io/py/factlite)
45
+ [![Build Status](https://travis-ci.org/your-username/factlite.svg?branch=main)](https://travis-ci.org/your-username/factlite)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
47
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](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
+ 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.
@@ -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
+ [![PyPI version](https://badge.fury.io/py/factlite.svg)](https://badge.fury.io/py/factlite)
45
+ [![Build Status](https://travis-ci.org/your-username/factlite.svg?branch=main)](https://travis-ci.org/your-username/factlite)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
47
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](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,173 @@
1
+ # FactLite 🪶
2
+
3
+ English | [中文](README_CN.md)
4
+
5
+ **Give Your LLM a "System 2" Brain with a Single Decorator.**
6
+
7
+ [![PyPI version](https://badge.fury.io/py/factlite.svg)](https://badge.fury.io/py/factlite)
8
+ [![Build Status](https://travis-ci.org/your-username/factlite.svg?branch=main)](https://travis-ci.org/your-username/factlite)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+