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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.10
3.11.6
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},

"editor.lineNumbersMinChars": 1,
"editor.glyphMargin": true,
"editor.folding": false,


"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
Expand All @@ -37,5 +42,9 @@
"autoDocstring.startOnNewLine": true,

"git.enableSmartCommit": true,
"git.confirmSync": false
"git.confirmSync": false,
"vim.showMarksInGutter": true,
"debug.allowBreakpointsEverywhere": true,
"debug.showBreakpointsInOverviewRuler": true,
"scm.diffDecorationsGutterWidth": 3
}
147 changes: 118 additions & 29 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,118 @@
import chainlit as cl

from src.verifact_manager import VerifactManager
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

Comment on lines +3 to 7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate logger setup; central setup_logging() is ignored

logging.basicConfig() here overrides the richer configuration in src/utils/logging/logging_config.py. Import and call setup_logging() instead of re-initialising the root logger:

-import logging
-
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
+from src.utils.logging.logging_config import setup_logging
+
+setup_logging()
+logger = logging.getLogger(__name__)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app.py around lines 3 to 7, remove the duplicate logging setup using
logging.basicConfig and logger = logging.getLogger(__name__). Instead, import
the setup_logging() function from src/utils/logging/logging_config.py and call
it at the start of the file to initialize logging with the centralized, richer
configuration.

pipeline = VerifactManager()


@cl.on_message
async def handle_message(message: cl.Message):
progress_msg = cl.Message(content="Starting fact-checking pipeline...")
await progress_msg.send()
progress_updates = []
# This dictionary will hold ALL our step objects for the duration of the run
steps = {}

# This will act like a stack to keep track of the current active step
active_step_id_stack = []

async def progress_callback(type: str, data: dict):

Check warning on line 19 in app.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

app.py#L19

Redefining built-in 'type'
nonlocal steps, active_step_id_stack

if type == "step_start":
parent_id = active_step_id_stack[-1] if active_step_id_stack else None
logger.info(f"Starting step: {data['title']}")
step = cl.Step(name=data["title"], parent_id=parent_id, id=data["title"])
steps[data["title"]] = step
active_step_id_stack.append(step.id) # Push new step to stack
Comment on lines +25 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Step IDs should be globally unique – titles are not

Using id=data["title"] risks collisions when two concurrent conversations both create a "Fact-Checking Pipeline" step. Let Chainlit auto-generate IDs or use uuid4():

-from uuid import uuid4
-...
-            step = cl.Step(name=data["title"], parent_id=parent_id, id=data["title"])
+from uuid import uuid4
+...
+            step = cl.Step(name=data["title"], parent_id=parent_id, id=str(uuid4()))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
step = cl.Step(name=data["title"], parent_id=parent_id, id=data["title"])
steps[data["title"]] = step
active_step_id_stack.append(step.id) # Push new step to stack
from uuid import uuid4
# … other code …
step = cl.Step(
name=data["title"],
parent_id=parent_id,
- id=data["title"]
+ id=str(uuid4())
)
steps[data["title"]] = step
active_step_id_stack.append(step.id) # Push new step to stack
🤖 Prompt for AI Agents
In app.py around lines 25 to 27, the step ID is set to the step title, which can
cause collisions if multiple steps share the same title. To fix this, remove the
explicit id assignment using the title and either let Chainlit auto-generate the
ID or assign a unique ID using uuid4(). This ensures all step IDs are globally
unique and prevents conflicts in concurrent conversations.

logger.info(f"Step stack after push: {active_step_id_stack}")
await step.send()

elif type == "step_end":
step_id = active_step_id_stack.pop() # Pop current step from stack
step = steps[step_id]
step.output = data["output"]
await step.update()

async def progress_callback(msg, update):
progress_updates.append(update)
msg.content = "\n".join(progress_updates)
await msg.update()
elif type == "step_error":
step_id = active_step_id_stack.pop() # Pop current step from stack
step = steps[step_id]
step.is_error = True
step.output = data["output"]
await step.update()

elif type == "claims_detected":
# This logic is special and doesn't use the stack
# It just adds claim-specific steps for later nesting
main_pipeline_id = steps["Fact-Checking Pipeline"].id
for i, claim in enumerate(data["claims"]):
claim_id = f"claim_{i+1}"
claim_step = cl.Step(
name=f'Claim {i+1}: "{claim.text[:60]}..."',
parent_id=main_pipeline_id,
id=claim_id,
)
steps[claim_id] = claim_step
Comment on lines +49 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same collision risk for per-claim steps

claim_1, claim_2, … will collide across sessions. Omit the id parameter entirely; you already store the Step object in steps[claim_id].

🤖 Prompt for AI Agents
In app.py around lines 49 to 55, the claim step IDs like "claim_1", "claim_2"
risk colliding across sessions. To fix this, remove the explicit id parameter
from the Step constructor when creating claim_step. Since you already store each
Step object in the steps dictionary keyed by claim_id, you don't need to assign
an id to the Step itself. This avoids ID collisions while preserving access to
each step.

await claim_step.send()

# The main logic starts here
await progress_callback(type="step_start", data={"title": "Fact-Checking Pipeline"})

try:
verdicts = await pipeline.run(message.content, progress_callback=progress_callback, progress_msg=progress_msg)
verdicts = await pipeline.run(
message.content, progress_callback=progress_callback
)

# Debug logging to see what we got back
logger.info(f"DEBUG: Received {len(verdicts) if verdicts else 0} verdicts")
logger.info(f"DEBUG: Verdicts content: {verdicts}")

if not verdicts:
progress_msg.content = "No factual claims detected in your message."
await progress_msg.update()
await progress_callback(
type="step_end",
data={"output": "No factual claims were detected in your message."},
)
return
# Format the final organized message
response = ""
for idx, (claim, evidence, verdict) in enumerate(verdicts):
claim_text = getattr(claim, 'text', str(claim))
verdict_text = getattr(verdict, 'verdict', str(verdict))
confidence = getattr(verdict, 'confidence', 'N/A')
explanation = getattr(verdict, 'explanation', 'N/A')
sources = getattr(verdict, 'sources', [])

# Format the final message and send it
response = "## Fact-Checking Complete\nHere are the results:\n"

for idx, verdict_tuple in enumerate(verdicts):
logger.info(f"DEBUG: Processing verdict {idx}: {verdict_tuple}")

# Handle different possible tuple structures
if len(verdict_tuple) == 3:
claim, evidence, verdict = verdict_tuple
else:
logger.error(f"Unexpected verdict tuple structure: {verdict_tuple}")
continue

# Safely extract attributes with fallbacks
claim_text = (
getattr(claim, "text", str(claim)) if claim else "Unknown claim"
)
verdict_text = (
getattr(verdict, "verdict", str(verdict)) if verdict else "No verdict"
)
confidence = getattr(verdict, "confidence", "N/A") if verdict else "N/A"
explanation = (
getattr(verdict, "explanation", "No explanation provided")
if verdict
else "N/A"
)
sources = getattr(verdict, "sources", []) if verdict else []
sources_str = "\n".join(sources) if sources else "No sources provided."
# Evidence formatting

if evidence:
evidence_str = "\n".join([
f"- {getattr(ev, 'content', str(ev))} (Source: {getattr(ev, 'source', 'N/A')}, Stance: {getattr(ev, 'stance', 'N/A')}, Relevance: {getattr(ev, 'relevance', 'N/A')})"
for ev in evidence
])
evidence_str = "\n".join(
[
f"- {getattr(ev, 'content', str(ev))} (Source: {getattr(ev, 'source', 'N/A')}, Stance: {getattr(ev, 'stance', 'N/A')}, Relevance: {getattr(ev, 'relevance', 'N/A')})"
for ev in evidence
]
)
else:
evidence_str = "No evidence found."

response += (
f"\n---\n**Claim {idx+1}:** {claim_text}\n"
f"**Evidence:**\n{evidence_str}\n"
Expand All @@ -46,12 +121,26 @@
f"**Explanation:** {explanation}\n"
f"**Sources:**\n{sources_str}\n"
)
progress_msg.content = response
await progress_msg.update()

# Close the main pipeline step and set its final output
steps["Fact-Checking Pipeline"].output = response
await steps["Fact-Checking Pipeline"].update()
active_step_id_stack.pop() # Final pop for the main step

Comment on lines +125 to +129
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Main step never gets a formal step_end event

You update the output and pop the stack, but the step remains in “running” state in the UI. Close it via the callback for consistency:

-        steps["Fact-Checking Pipeline"].output = response
-        await steps["Fact-Checking Pipeline"].update()
-        active_step_id_stack.pop()  # Final pop for the main step
+        await progress_callback(
+            event_type="step_end",
+            data={"output": response},
+        )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Close the main pipeline step and set its final output
steps["Fact-Checking Pipeline"].output = response
await steps["Fact-Checking Pipeline"].update()
active_step_id_stack.pop() # Final pop for the main step
# Close the main pipeline step and set its final output
- steps["Fact-Checking Pipeline"].output = response
- await steps["Fact-Checking Pipeline"].update()
- active_step_id_stack.pop() # Final pop for the main step
+ await progress_callback(
+ event_type="step_end",
+ data={"output": response},
+ )
🤖 Prompt for AI Agents
In app.py around lines 125 to 129, the main pipeline step is updated and popped
from the active step stack but never formally closed with a step_end event,
causing it to remain in a "running" state in the UI. To fix this, invoke the
step_end callback or method on the "Fact-Checking Pipeline" step before popping
it from the stack to properly close the step and update its state.

# Send a final, separate message for easy viewing
await cl.Message(content=response).send()

except Exception as e:
progress_msg.content = f"An error occurred during fact-checking: {str(e)}"
await progress_msg.update()
logger.error(f"An error occurred in the main pipeline: {e}", exc_info=True)
# Check if there's an active step to mark as error
if active_step_id_stack:
await progress_callback(
type="step_error", data={"output": f"An error occurred: {str(e)}"}
)


@cl.on_chat_start
async def on_chat_start():
await cl.Message(content="👋 Welcome to VeriFact! The system is up and running. Type your claim or question to get started.").send()
await cl.Message(
content="👋 Welcome to VeriFact! The system is up and running. Type your claim or question to get started."
).send()
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ dependencies = [
"beautifulsoup4>=4.12.0",
"pytest-asyncio>=0.21.0",
"psutil",
"chainlit",
"openai-agents>=0.0.15",
"chainlit>=1.0",
"openai-agents<=0.0.16",
"serpapi>=0.1.5",
]

Expand Down
133 changes: 133 additions & 0 deletions src/tests/test_claim_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import pytest
from pydantic import ValidationError

# Adjust the import path based on your project structure.
# This assumes 'src' is in your PYTHONPATH or you're running pytest from the project root.
from verifact_agents.claim_detector import Claim, claim_detector_agent, PROMPT

# --- Fixtures ---
# Fixtures are a way to provide data or set up resources for your tests.

@pytest.fixture
def valid_claim_data() -> dict:
"""Provides a dictionary with valid data for a Claim instance."""
return {
"text": "The Earth is round.",
"normalized_text": "The Earth has a spherical shape.",
"check_worthiness_score": 0.9,
"specificity_score": 0.8,
"public_interest_score": 0.7,
"impact_score": 0.6,
"detection_confidence": 0.95,
"domain": "Science",
"entities": [{"text": "Earth", "type": "Planet"}],
"compound_claim_parts": None,
"rank": 1
}

# --- Tests for the Claim Pydantic Model ---

def test_claim_creation_valid_data(valid_claim_data):
"""Test that a Claim can be successfully created with valid data."""
try:
claim = Claim(**valid_claim_data)
# Check a few key fields to ensure data is loaded correctly
assert claim.text == valid_claim_data["text"]
assert claim.normalized_text == valid_claim_data["normalized_text"]
assert claim.check_worthiness_score == valid_claim_data["check_worthiness_score"]
assert claim.rank == valid_claim_data["rank"]
assert claim.entities == valid_claim_data["entities"]
except ValidationError as e:
pytest.fail(f"Claim creation failed with valid data: {e}")

def test_claim_missing_required_field(valid_claim_data):
"""Test that ValidationError is raised if a required field (e.g., 'text') is missing."""
invalid_data = valid_claim_data.copy()
del invalid_data["text"] # 'text' is a required field

# pytest.raises is a context manager to check for expected exceptions
with pytest.raises(ValidationError) as excinfo:
Claim(**invalid_data)

# Optionally, you can inspect the exception details
assert "text" in str(excinfo.value).lower() # Check that the error message mentions 'text'
assert "field required" in str(excinfo.value).lower()

@pytest.mark.parametrize("score_field,invalid_value", [
("check_worthiness_score", -0.1),
("check_worthiness_score", 1.1),
("specificity_score", -0.5),
("specificity_score", 1.5),


("public_interest_score", -0.01),
("public_interest_score", 2.0),
("impact_score", -1.0),
("impact_score", 1.0001),
("detection_confidence", -0.2),
("detection_confidence", 1.2),
])
def test_claim_score_out_of_range(valid_claim_data, score_field, invalid_value):
"""Test that scores must be between 0.0 and 1.0."""
invalid_data = valid_claim_data.copy()
invalid_data[score_field] = invalid_value

with pytest.raises(ValidationError) as excinfo:
Claim(**invalid_data)

# Check that the error message mentions the problematic field
assert score_field in str(excinfo.value)

def test_claim_default_values(valid_claim_data):
"""Test default values for optional fields like 'entities' and 'compound_claim_parts'."""
data = valid_claim_data.copy()
del data["entities"] # entities has default_factory=[]
del data["compound_claim_parts"] # compound_claim_parts has default=None
print("====>data:", data)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove debug print statement.

Debug print statements should not be committed to the test suite.

-    print("====>data:", data)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
print("====>data:", data)
🤖 Prompt for AI Agents
In src/tests/test_claim_detector.py at line 86, remove the debug print statement
"print("====>data:", data)" to keep the test suite clean and free of debugging
output.


claim = Claim(**data)

assert claim.entities == []
assert claim.compound_claim_parts is None

def test_claim_extra_fields_forbidden(valid_claim_data):
"""Test that extra fields are not allowed due to model_config = {'extra': 'forbid'}."""
data_with_extra = valid_claim_data.copy()
data_with_extra["unexpected_field"] = "some_value"

with pytest.raises(ValidationError) as excinfo:
Claim(**data_with_extra)

assert "unexpected_field" in str(excinfo.value)
assert "extra inputs are not permitted" in str(excinfo.value).lower()

def test_claim_invalid_data_type_for_field(valid_claim_data):
"""Test that providing an incorrect data type for a field raises ValidationError."""
invalid_data = valid_claim_data.copy()
invalid_data["rank"] = "not-an-integer" # rank should be an int

with pytest.raises(ValidationError) as excinfo:
Claim(**invalid_data)
assert "rank" in str(excinfo.value) # Check that the error message mentions 'rank'

# --- Tests for the claim_detector_agent Instance ---

def test_claim_detector_agent_instantiation():
"""Test that the claim_detector_agent is instantiated and has basic properties."""
assert claim_detector_agent is not None
assert claim_detector_agent.name == "ClaimDetector"

assert claim_detector_agent.output_type.__origin__ == list # Checks if it's a list type
assert claim_detector_agent.output_type.__args__[0] == Claim # Checks if the list contains Claim
Comment on lines +120 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use proper type checking methods.

Direct comparison with == for types is not recommended. Use isinstance() or is for type checks.

-    assert claim_detector_agent.output_type.__origin__ == list # Checks if it's a list type
-    assert claim_detector_agent.output_type.__args__[0] == Claim # Checks if the list contains Claim
+    assert claim_detector_agent.output_type.__origin__ is list # Checks if it's a list type
+    assert claim_detector_agent.output_type.__args__[0] is Claim # Checks if the list contains Claim
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert claim_detector_agent.output_type.__origin__ == list # Checks if it's a list type
assert claim_detector_agent.output_type.__args__[0] == Claim # Checks if the list contains Claim
assert claim_detector_agent.output_type.__origin__ is list # Checks if it's a list type
assert claim_detector_agent.output_type.__args__[0] is Claim # Checks if the list contains Claim
🧰 Tools
🪛 Ruff (0.11.9)

120-120: Use is and is not for type comparisons, or isinstance() for isinstance checks

(E721)

🤖 Prompt for AI Agents
In src/tests/test_claim_detector.py at lines 120 to 121, replace the direct
equality checks (==) for type comparison with proper type checking using 'is'
for identity comparison. Change the assertions to use 'is' instead of '==' when
comparing __origin__ and __args__[0] to ensure correct and recommended type
checking.


# Check if instructions are loaded (at least that it's not empty)
assert claim_detector_agent.instructions == PROMPT
assert PROMPT.strip() != "" # Ensure the PROMPT string itself is not empty

# Note: Testing the agent's actual processing (which calls an LLM)
# is an integration test and would require mocking os.getenv or the Agent's call method.
# For an MVP unit test, checking instantiation and configuration is a good start.

# You can add more tests here, for example:
# - Test specific constraints on 'entities' (e.g., must be list of dicts with 'text' and 'type')
# - Test 'compound_claim_parts' (e.g., must be list of strings if not None)
2 changes: 1 addition & 1 deletion src/utils/logging/logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ def setup_logging():
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
root_logger.handlers = handlers
root_logger.propagate = False
root_logger.propagate = False
Loading