|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
from rich.text import Text
|
|
from rich.padding import Padding
|
|
import data_models
|
|
import nlp_utils
|
|
import argument_analyzer
|
|
import logic_analyzer
|
|
|
|
import rhetoric_analyzer
|
|
import synthesis_engine
|
|
import argument_visualizer
|
|
import networkx as nx
|
|
from typing import Optional, List
|
|
import sys
|
|
import textwrap
|
|
|
|
console = Console()
|
|
sentences = []
|
|
|
|
|
|
def format_sentence_with_highlights(
|
|
sentence_idx: int, all_sentences: List[data_models.SentenceInfo],
|
|
findings: List[data_models.Finding], components: List[data_models.ArgumentComponent]
|
|
) -> Text:
|
|
|
|
if sentence_idx < 0 or sentence_idx >= len(all_sentences): return Text("(Error: Invalid sentence index)")
|
|
sentence = all_sentences[sentence_idx]; text = Text(sentence.text)
|
|
for comp in components:
|
|
if comp.sentence_index == sentence_idx:
|
|
style = "bold magenta" if comp.component_type == "Claim" else "magenta"
|
|
start = comp.span_start - sentence.start_char; end = comp.span_end - sentence.start_char
|
|
if 0 <= start < end <= len(sentence.text):
|
|
try: text.stylize(style, start, end)
|
|
except Exception as e: console.print(f"[dim yellow]Warn: Styling component ({start}-{end}) in sent {sentence_idx}: {e}[/dim]")
|
|
for finding in findings:
|
|
if finding.span_start == sentence.start_char and finding.span_end == sentence.end_char:
|
|
prefix = "[F] " if finding.finding_type == "Fallacy" else \
|
|
"[R] " if finding.finding_type == "RhetoricalDevice" else "[?] "
|
|
style = "bold red" if prefix=="[F] " else "bold yellow" if prefix=="[R] " else ""
|
|
text.insert(0, prefix, style=style)
|
|
return text
|
|
|
|
|
|
def main(
|
|
text: Optional[str] = typer.Option(None, "--text", "-t", help="Text to analyze directly."),
|
|
file_path: Optional[str] = typer.Option(None, "--file", "-f", help="Path to a text file to analyze."),
|
|
max_findings_display: int = typer.Option(5, "--max-findings", "-m", help="Max number of each finding type to display in detail.")
|
|
):
|
|
"""
|
|
ETHOS: The AI Arbiter of Rational Discourse (CLI) - v2.2
|
|
Semantic Argument Linking & AI Fallacy Integration.
|
|
"""
|
|
console.print(Panel("[bold cyan]ETHOS Analysis Engine v2.2 Starting...[/bold cyan]", expand=False, border_style="cyan"))
|
|
|
|
if text and file_path: console.print("[bold red]Error: Use --text OR --file, not both.[/bold red]"); raise typer.Exit(code=1)
|
|
if not text and not file_path: console.print("[bold red]Error: Use --text '...' OR --file '...'[/bold red]"); raise typer.Exit(code=1)
|
|
text_to_analyze = ""; input_source_msg = ""
|
|
if file_path:
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f: text_to_analyze = f.read()
|
|
input_source_msg = f"File: [yellow]'{file_path}'[/yellow]"
|
|
except Exception as e: console.print(f"[bold red]Error reading file '{file_path}': {e}[/bold red]"); raise typer.Exit(code=1)
|
|
elif text:
|
|
text_to_analyze = text; input_source_msg = f"Input Text ({len(text_to_analyze)} chars)"
|
|
if not text_to_analyze.strip(): console.print("[bold red]Error: Input text is empty.[/bold red]"); raise typer.Exit(code=1)
|
|
console.print(Padding(f"Analyzing: {input_source_msg}", (0, 1)))
|
|
|
|
|
|
console.print("\n[bold blue]--- Initializing Analyzers & Embeddings ---[/bold blue]")
|
|
try:
|
|
nlp_utils.load_spacy_model(); nlp_utils.load_bert()
|
|
spacy_doc = nlp_utils.process_text_spacy(text_to_analyze)
|
|
|
|
sentence_embeddings = nlp_utils.get_all_sentence_embeddings(spacy_doc)
|
|
except Exception as e: console.print(f"[bold red]Error during model loading/processing/embedding: {e}[/bold red]"); raise typer.Exit(code=1)
|
|
if not spacy_doc: console.print("[bold red]Error: spaCy doc creation failed.[/bold red]"); raise typer.Exit(code=1)
|
|
|
|
|
|
console.print("\n[bold blue]--- Running Analysis Modules ---[/bold blue]")
|
|
global sentences
|
|
sentences = [data_models.SentenceInfo(text=s.text, start_char=s.start_char, end_char=s.end_char, tokens=[t.text for t in s]) for s in spacy_doc.sents]
|
|
|
|
console.print("[cyan]Running Argument Analyzer (Enhanced)...[/cyan]")
|
|
argument_components = argument_analyzer.enhanced_component_analyzer(spacy_doc)
|
|
|
|
console.print("[cyan]Running Logic Analyzer (Enhanced - Rules + ML)...[/cyan]")
|
|
fallacy_findings = logic_analyzer.enhanced_fallacy_analyzer(spacy_doc)
|
|
|
|
evidence_findings = []
|
|
|
|
console.print("[cyan]Running Rhetoric Analyzer...[/cyan]")
|
|
rhetoric_findings = rhetoric_analyzer.simple_rhetoric_analyzer(spacy_doc)
|
|
|
|
console.print("[cyan]Running Synthesis Engine (Evidence Excluded)...[/cyan]")
|
|
all_findings: List[data_models.Finding] = fallacy_findings + rhetoric_findings
|
|
analysis_summary = synthesis_engine.generate_summary_ratings(argument_components, all_findings)
|
|
|
|
|
|
console.print("[cyan]Building Argument Graph (Semantic Linking)...[/cyan]")
|
|
|
|
argument_graph = argument_visualizer.build_argument_graph(argument_components, sentence_embeddings)
|
|
graph_text_representation = argument_visualizer.format_graph_text(argument_graph)
|
|
|
|
|
|
analysis_result = data_models.AnalyzedText(
|
|
original_text=text_to_analyze, sentences=sentences,
|
|
argument_components=argument_components, findings=all_findings,
|
|
analysis_summary=analysis_summary
|
|
)
|
|
|
|
|
|
console.rule("[bold green]ETHOS Analysis Report[/bold green]", style="green")
|
|
|
|
summary_table = Table(title="Analysis Summary", show_header=False, box=None, padding=(0, 1))
|
|
|
|
if analysis_result.analysis_summary:
|
|
for category, rating in analysis_result.analysis_summary.items():
|
|
style = "red" if rating.startswith(("Low", "Weak", "Questionable")) else "yellow" if rating.startswith(("Medium", "Moderate", "Mixed")) else "green"
|
|
if rating == "Not Evaluated": style = "dim"
|
|
summary_table.add_row(category, f"[{style}]{rating}[/{style}]")
|
|
else: summary_table.add_row("Summary", "[dim]Not generated.[/dim]")
|
|
console.print(Padding(summary_table, (1, 0)))
|
|
|
|
|
|
|
|
console.print("\n[bold underline]Detected Findings:[/bold underline]")
|
|
if not analysis_result.findings: console.print(Padding(" No significant findings detected.", (0, 2)))
|
|
else:
|
|
|
|
grouped_findings = {};
|
|
for f in analysis_result.findings: grouped_findings.setdefault(f.finding_type, []).append(f)
|
|
grouped_findings.pop("EvidenceIndicator", None); grouped_findings.pop("EvidenceStatus", None)
|
|
if not grouped_findings: console.print(Padding(" No significant findings detected.", (0, 2)))
|
|
else:
|
|
for f_type, findings_list in grouped_findings.items():
|
|
color = "red" if f_type=="Fallacy" else "yellow" if f_type=="RhetoricalDevice" else "white"
|
|
console.print(Padding(f"[bold {color}]{f_type} Indicators ({len(findings_list)} found):[/bold {color}]", (1, 1)))
|
|
for i, finding in enumerate(findings_list[:max_findings_display]):
|
|
details_dict = finding.details if finding.details else {}
|
|
details_text = details_dict.get('fallacy_type') or details_dict.get('device_type') or 'Details N/A'
|
|
trigger_text = details_dict.get('trigger') or details_dict.get('words')
|
|
confidence = details_dict.get('confidence'); model_used = details_dict.get('model_used')
|
|
details_str = f"({details_text}"
|
|
if trigger_text and not model_used: details_str += f", Trigger: '{textwrap.shorten(str(trigger_text), width=25, placeholder='...')}'"
|
|
if confidence is not None: details_str += f", Score: {confidence:.2f}"
|
|
if model_used: details_str += f", Model: '{model_used.split('/')[-1]}'"
|
|
details_str += ")"
|
|
console.print(Padding(f"{i+1}. {finding.description} {details_str}", (0, 3)))
|
|
try:
|
|
sentence_idx = next(idx for idx, s in enumerate(analysis_result.sentences) if s.start_char == finding.span_start)
|
|
related_sentence_text = analysis_result.sentences[sentence_idx].text
|
|
console.print(Padding(f"[dim] In Sent {sentence_idx+1}: \"{textwrap.shorten(related_sentence_text, width=90, placeholder='...')}\"[/dim]", (0, 5)))
|
|
except StopIteration: console.print(Padding(f"[dim] (Could not pinpoint exact sentence for span starting at char {finding.span_start})[/dim]", (0,5)))
|
|
if len(findings_list) > max_findings_display: console.print(Padding(f"... and {len(findings_list) - max_findings_display} more.", (0, 3)))
|
|
|
|
|
|
|
|
console.print("\n[bold underline]Identified Argument Components:[/bold underline]")
|
|
if not analysis_result.argument_components: console.print(Padding(" No argument components identified.", (0, 2)))
|
|
else:
|
|
|
|
comp_table = Table(title=None, show_header=True, header_style="bold magenta", box=None, padding=(0,1))
|
|
comp_table.add_column("Type", style="magenta", width=10); comp_table.add_column("Text Snippet (Confidence)")
|
|
for comp in analysis_result.argument_components:
|
|
conf_str = f"({comp.confidence:.2f})" if comp.confidence is not None else ""
|
|
comp_table.add_row(comp.component_type, f"\"{textwrap.shorten(comp.text, width=90, placeholder='...')}\" {conf_str}")
|
|
console.print(Padding(comp_table, (1, 1)))
|
|
|
|
|
|
|
|
console.print("\n[bold underline]Argument Structure Visualization (Semantic):[/bold underline]")
|
|
console.print(Padding(graph_text_representation, (0, 1)))
|
|
|
|
console.print(Padding(f"[dim](Note: Links based on semantic similarity >= {argument_visualizer.LINKING_SIMILARITY_THRESHOLD})[/dim]", (0,1)))
|
|
|
|
|
|
console.rule(style="green")
|
|
console.print(f"(V2 Semantic Argument Linking. Total potential findings: {len(analysis_result.findings)})")
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
typer.run(main) |