feat(cli): implement comprehensive CLI commands for investigate, analyze and report
Browse files- Implement 'cidadao investigate' command with full API integration
- Real-time investigation execution with progress tracking
- Multiple output formats (table, markdown, json, html)
- Save results to file with custom naming
- Rich terminal UI with color-coded severity levels
- Implement 'cidadao analyze' command for pattern detection
- Support for temporal, supplier, category, regional analyses
- Interactive dashboard display with statistics
- Export results to CSV with multiple files
- Correlation detection with visual strength indicators
- Implement 'cidadao report' command for document generation
- Multiple report types (investigation, executive, audit, etc)
- Multi-format output (PDF, Excel, Markdown, HTML, JSON)
- Subcommands for download and list operations
- Real-time generation progress tracking
All commands feature:
- Rich terminal UI with progress bars and styled output
- Async API integration with proper error handling
- Environment variable support for API keys
- Comprehensive help documentation with examples
- src/cli/commands/__init__.py +7 -7
- src/cli/commands/analyze.py +511 -34
- src/cli/commands/investigate.py +430 -31
- src/cli/commands/report.py +476 -37
- src/cli/main.py +6 -6
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""CLI commands for Cidad�o.AI.
|
| 2 |
|
| 3 |
This module provides command-line interface commands for:
|
| 4 |
- Investigation operations
|
|
@@ -9,14 +9,14 @@ This module provides command-line interface commands for:
|
|
| 9 |
Status: Stub implementation - Full CLI planned for production phase.
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
from .investigate import
|
| 13 |
-
from .analyze import
|
| 14 |
-
from .report import
|
| 15 |
from .watch import watch_command
|
| 16 |
|
| 17 |
__all__ = [
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
| 21 |
"watch_command"
|
| 22 |
]
|
|
|
|
| 1 |
+
"""CLI commands for Cidad�o.AI.
|
| 2 |
|
| 3 |
This module provides command-line interface commands for:
|
| 4 |
- Investigation operations
|
|
|
|
| 9 |
Status: Stub implementation - Full CLI planned for production phase.
|
| 10 |
"""
|
| 11 |
|
| 12 |
+
from .investigate import investigate
|
| 13 |
+
from .analyze import analyze
|
| 14 |
+
from .report import report
|
| 15 |
from .watch import watch_command
|
| 16 |
|
| 17 |
__all__ = [
|
| 18 |
+
"investigate",
|
| 19 |
+
"analyze",
|
| 20 |
+
"report",
|
| 21 |
"watch_command"
|
| 22 |
]
|
|
@@ -1,44 +1,521 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
-
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if period:
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
click.echo(f"💾 Salvando em: {save}")
|
| 37 |
|
| 38 |
-
|
| 39 |
-
click.echo("⚠️ Funcionalidade em desenvolvimento")
|
| 40 |
-
click.echo("📋 Status: Implementação planejada para fase de produção")
|
| 41 |
|
| 42 |
|
| 43 |
-
if __name__ ==
|
| 44 |
-
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module: cli.commands.analyze
|
| 3 |
+
Description: Analysis command for CLI - pattern and correlation analysis
|
| 4 |
+
Author: Anderson H. Silva
|
| 5 |
+
Date: 2025-01-25
|
| 6 |
+
License: Proprietary - All rights reserved
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
from datetime import datetime, timedelta
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional, List, Dict, Any
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
import typer
|
| 16 |
+
from rich.console import Console
|
| 17 |
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
| 18 |
+
from rich.table import Table
|
| 19 |
+
from rich.panel import Panel
|
| 20 |
+
from rich import box
|
| 21 |
+
from rich.columns import Columns
|
| 22 |
+
from rich.syntax import Syntax
|
| 23 |
+
import httpx
|
| 24 |
+
from pydantic import BaseModel, Field
|
| 25 |
+
import pandas as pd
|
| 26 |
+
|
| 27 |
+
# CLI app
|
| 28 |
+
app = typer.Typer(help="Analyze patterns and correlations in government data")
|
| 29 |
+
console = Console()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class AnalysisType(str, Enum):
|
| 33 |
+
"""Analysis type options."""
|
| 34 |
+
TEMPORAL = "temporal"
|
| 35 |
+
SUPPLIER = "supplier"
|
| 36 |
+
CATEGORY = "category"
|
| 37 |
+
REGIONAL = "regional"
|
| 38 |
+
CORRELATION = "correlation"
|
| 39 |
+
COMPREHENSIVE = "comprehensive"
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class OutputFormat(str, Enum):
|
| 43 |
+
"""Output format options."""
|
| 44 |
+
DASHBOARD = "dashboard"
|
| 45 |
+
TABLE = "table"
|
| 46 |
+
JSON = "json"
|
| 47 |
+
CSV = "csv"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class AnalysisRequest(BaseModel):
|
| 51 |
+
"""Analysis request model."""
|
| 52 |
+
analysis_type: AnalysisType
|
| 53 |
+
start_date: Optional[datetime] = None
|
| 54 |
+
end_date: Optional[datetime] = None
|
| 55 |
+
organizations: List[str] = Field(default_factory=list)
|
| 56 |
+
suppliers: List[str] = Field(default_factory=list)
|
| 57 |
+
categories: List[str] = Field(default_factory=list)
|
| 58 |
+
regions: List[str] = Field(default_factory=list)
|
| 59 |
+
min_value: Optional[float] = None
|
| 60 |
+
max_value: Optional[float] = None
|
| 61 |
+
include_trends: bool = True
|
| 62 |
+
include_outliers: bool = True
|
| 63 |
+
correlation_threshold: float = 0.7
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class AnalysisResult(BaseModel):
|
| 67 |
+
"""Analysis result model."""
|
| 68 |
+
id: str
|
| 69 |
+
analysis_type: str
|
| 70 |
+
created_at: datetime
|
| 71 |
+
status: str
|
| 72 |
+
summary: Dict[str, Any]
|
| 73 |
+
patterns: List[Dict[str, Any]]
|
| 74 |
+
correlations: List[Dict[str, Any]]
|
| 75 |
+
trends: List[Dict[str, Any]]
|
| 76 |
+
outliers: List[Dict[str, Any]]
|
| 77 |
+
statistics: Dict[str, Any]
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
async def call_api(
|
| 81 |
+
endpoint: str,
|
| 82 |
+
method: str = "GET",
|
| 83 |
+
data: Optional[Dict[str, Any]] = None,
|
| 84 |
+
params: Optional[Dict[str, Any]] = None,
|
| 85 |
+
auth_token: Optional[str] = None
|
| 86 |
+
) -> Dict[str, Any]:
|
| 87 |
+
"""Make API call to backend."""
|
| 88 |
+
api_url = "http://localhost:8000"
|
| 89 |
+
|
| 90 |
+
headers = {
|
| 91 |
+
"Content-Type": "application/json",
|
| 92 |
+
"User-Agent": "Cidadao.AI-CLI/1.0"
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if auth_token:
|
| 96 |
+
headers["Authorization"] = f"Bearer {auth_token}"
|
| 97 |
+
|
| 98 |
+
async with httpx.AsyncClient() as client:
|
| 99 |
+
response = await client.request(
|
| 100 |
+
method=method,
|
| 101 |
+
url=f"{api_url}{endpoint}",
|
| 102 |
+
headers=headers,
|
| 103 |
+
json=data,
|
| 104 |
+
params=params,
|
| 105 |
+
timeout=120.0 # Longer timeout for analysis
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
if response.status_code >= 400:
|
| 109 |
+
error_detail = response.json().get("detail", "Unknown error")
|
| 110 |
+
raise Exception(f"API Error: {error_detail}")
|
| 111 |
+
|
| 112 |
+
return response.json()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def display_dashboard(result: AnalysisResult):
|
| 116 |
+
"""Display analysis results as a dashboard."""
|
| 117 |
+
# Title
|
| 118 |
+
console.print(
|
| 119 |
+
Panel(
|
| 120 |
+
f"[bold blue]📊 Analysis Dashboard[/bold blue]\n"
|
| 121 |
+
f"Type: {result.analysis_type.upper()}\n"
|
| 122 |
+
f"Generated: {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
| 123 |
+
title="Cidadão.AI Analysis",
|
| 124 |
+
border_style="blue"
|
| 125 |
+
)
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
# Summary statistics
|
| 129 |
+
stats = result.statistics
|
| 130 |
+
console.print("\n[bold]📈 Summary Statistics:[/bold]")
|
| 131 |
+
|
| 132 |
+
stats_table = Table(show_header=True, header_style="bold cyan", box=box.ROUNDED)
|
| 133 |
+
stats_table.add_column("Metric", style="dim")
|
| 134 |
+
stats_table.add_column("Value", justify="right")
|
| 135 |
+
|
| 136 |
+
for key, value in stats.items():
|
| 137 |
+
if isinstance(value, float):
|
| 138 |
+
stats_table.add_row(key.replace("_", " ").title(), f"{value:,.2f}")
|
| 139 |
+
else:
|
| 140 |
+
stats_table.add_row(key.replace("_", " ").title(), str(value))
|
| 141 |
+
|
| 142 |
+
console.print(stats_table)
|
| 143 |
+
|
| 144 |
+
# Patterns
|
| 145 |
+
if result.patterns:
|
| 146 |
+
console.print("\n[bold]🔍 Patterns Detected:[/bold]")
|
| 147 |
+
for i, pattern in enumerate(result.patterns[:5], 1):
|
| 148 |
+
console.print(
|
| 149 |
+
Panel(
|
| 150 |
+
f"[yellow]Pattern {i}: {pattern.get('name', 'Unknown')}[/yellow]\n"
|
| 151 |
+
f"Confidence: {pattern.get('confidence', 0):.2%}\n"
|
| 152 |
+
f"Description: {pattern.get('description', 'N/A')}\n"
|
| 153 |
+
f"Impact: {pattern.get('impact', 'N/A')}",
|
| 154 |
+
border_style="yellow"
|
| 155 |
+
)
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Top correlations
|
| 159 |
+
if result.correlations:
|
| 160 |
+
console.print("\n[bold]🔗 Strong Correlations:[/bold]")
|
| 161 |
+
corr_table = Table(show_header=True, header_style="bold green")
|
| 162 |
+
corr_table.add_column("Variable 1", width=25)
|
| 163 |
+
corr_table.add_column("Variable 2", width=25)
|
| 164 |
+
corr_table.add_column("Correlation", justify="center")
|
| 165 |
+
corr_table.add_column("Strength", justify="center")
|
| 166 |
+
|
| 167 |
+
for corr in result.correlations[:5]:
|
| 168 |
+
strength = abs(corr.get('value', 0))
|
| 169 |
+
color = "red" if strength >= 0.9 else "yellow" if strength >= 0.7 else "green"
|
| 170 |
+
corr_table.add_row(
|
| 171 |
+
corr.get('var1', 'N/A'),
|
| 172 |
+
corr.get('var2', 'N/A'),
|
| 173 |
+
f"{corr.get('value', 0):.3f}",
|
| 174 |
+
f"[{color}]{'●' * int(strength * 5)}[/{color}]"
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
console.print(corr_table)
|
| 178 |
+
|
| 179 |
+
# Trends
|
| 180 |
+
if result.trends:
|
| 181 |
+
console.print("\n[bold]📈 Key Trends:[/bold]")
|
| 182 |
+
for trend in result.trends[:3]:
|
| 183 |
+
direction = "↗️" if trend.get('direction') == 'up' else "↘️" if trend.get('direction') == 'down' else "→"
|
| 184 |
+
console.print(
|
| 185 |
+
f"{direction} [cyan]{trend.get('name', 'Unknown')}:[/cyan] "
|
| 186 |
+
f"{trend.get('description', 'N/A')} "
|
| 187 |
+
f"([dim]{trend.get('change', 0):+.1%}[/dim])"
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
# Outliers alert
|
| 191 |
+
if result.outliers:
|
| 192 |
+
console.print(f"\n[bold red]⚠️ {len(result.outliers)} outliers detected![/bold red]")
|
| 193 |
+
console.print("[dim]Use --show-outliers flag to see details[/dim]")
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def display_table(result: AnalysisResult):
|
| 197 |
+
"""Display results in table format."""
|
| 198 |
+
# Main results table
|
| 199 |
+
table = Table(title="Analysis Results", show_header=True, header_style="bold magenta")
|
| 200 |
+
table.add_column("Category", style="dim")
|
| 201 |
+
table.add_column("Items", justify="right")
|
| 202 |
+
|
| 203 |
+
table.add_row("Patterns Found", str(len(result.patterns)))
|
| 204 |
+
table.add_row("Correlations", str(len(result.correlations)))
|
| 205 |
+
table.add_row("Trends", str(len(result.trends)))
|
| 206 |
+
table.add_row("Outliers", str(len(result.outliers)))
|
| 207 |
|
| 208 |
+
console.print(table)
|
| 209 |
+
|
| 210 |
+
# Detailed patterns
|
| 211 |
+
if result.patterns:
|
| 212 |
+
console.print("\n[bold]Patterns:[/bold]")
|
| 213 |
+
patterns_table = Table(show_header=True)
|
| 214 |
+
patterns_table.add_column("Name", width=30)
|
| 215 |
+
patterns_table.add_column("Type", style="dim")
|
| 216 |
+
patterns_table.add_column("Confidence", justify="center")
|
| 217 |
+
patterns_table.add_column("Description", width=40)
|
| 218 |
+
|
| 219 |
+
for pattern in result.patterns[:10]:
|
| 220 |
+
patterns_table.add_row(
|
| 221 |
+
pattern.get('name', 'Unknown'),
|
| 222 |
+
pattern.get('type', 'N/A'),
|
| 223 |
+
f"{pattern.get('confidence', 0):.1%}",
|
| 224 |
+
pattern.get('description', 'N/A')[:40]
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
console.print(patterns_table)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def display_json(result: AnalysisResult):
|
| 231 |
+
"""Display results in JSON format."""
|
| 232 |
+
import json
|
| 233 |
+
json_str = json.dumps(result.dict(), indent=2, default=str, ensure_ascii=False)
|
| 234 |
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
| 235 |
+
console.print(syntax)
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def export_csv(result: AnalysisResult, filename: Path):
|
| 239 |
+
"""Export results to CSV files."""
|
| 240 |
+
# Export patterns
|
| 241 |
+
if result.patterns:
|
| 242 |
+
patterns_df = pd.DataFrame(result.patterns)
|
| 243 |
+
patterns_file = filename.parent / f"{filename.stem}_patterns.csv"
|
| 244 |
+
patterns_df.to_csv(patterns_file, index=False)
|
| 245 |
+
console.print(f"[green]✓ Patterns exported to: {patterns_file}[/green]")
|
| 246 |
+
|
| 247 |
+
# Export correlations
|
| 248 |
+
if result.correlations:
|
| 249 |
+
corr_df = pd.DataFrame(result.correlations)
|
| 250 |
+
corr_file = filename.parent / f"{filename.stem}_correlations.csv"
|
| 251 |
+
corr_df.to_csv(corr_file, index=False)
|
| 252 |
+
console.print(f"[green]✓ Correlations exported to: {corr_file}[/green]")
|
| 253 |
+
|
| 254 |
+
# Export statistics
|
| 255 |
+
stats_df = pd.DataFrame([result.statistics])
|
| 256 |
+
stats_file = filename.parent / f"{filename.stem}_statistics.csv"
|
| 257 |
+
stats_df.to_csv(stats_file, index=False)
|
| 258 |
+
console.print(f"[green]✓ Statistics exported to: {stats_file}[/green]")
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
@app.command()
|
| 262 |
+
def analyze(
|
| 263 |
+
analysis_type: AnalysisType = typer.Argument(help="Type of analysis to perform"),
|
| 264 |
+
period: Optional[str] = typer.Option(None, "--period", "-p", help="Analysis period (e.g., 2024, 2024-Q1, last-30-days)"),
|
| 265 |
+
organizations: Optional[List[str]] = typer.Option(None, "--org", "-o", help="Organization codes to analyze"),
|
| 266 |
+
suppliers: Optional[List[str]] = typer.Option(None, "--supplier", "-s", help="Supplier names to analyze"),
|
| 267 |
+
categories: Optional[List[str]] = typer.Option(None, "--category", "-c", help="Contract categories to analyze"),
|
| 268 |
+
regions: Optional[List[str]] = typer.Option(None, "--region", "-r", help="Regions to analyze"),
|
| 269 |
+
min_value: Optional[float] = typer.Option(None, "--min-value", help="Minimum contract value"),
|
| 270 |
+
max_value: Optional[float] = typer.Option(None, "--max-value", help="Maximum contract value"),
|
| 271 |
+
output: OutputFormat = typer.Option(OutputFormat.DASHBOARD, "--output", "-f", help="Output format"),
|
| 272 |
+
save: Optional[Path] = typer.Option(None, "--save", help="Save results to file"),
|
| 273 |
+
show_outliers: bool = typer.Option(False, "--show-outliers", help="Show detailed outlier information"),
|
| 274 |
+
correlation_threshold: float = typer.Option(0.7, "--corr-threshold", help="Minimum correlation threshold"),
|
| 275 |
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="CIDADAO_API_KEY", help="API key"),
|
| 276 |
+
):
|
| 277 |
"""
|
| 278 |
+
📊 Analyze patterns and correlations in government data.
|
| 279 |
|
| 280 |
+
This command performs deep analysis on government contracts and spending
|
| 281 |
+
to identify patterns, trends, correlations, and anomalies.
|
| 282 |
|
| 283 |
+
Analysis Types:
|
| 284 |
+
- temporal: Time-based patterns and seasonality
|
| 285 |
+
- supplier: Supplier behavior and concentration
|
| 286 |
+
- category: Spending by category analysis
|
| 287 |
+
- regional: Geographic distribution analysis
|
| 288 |
+
- correlation: Variable correlation analysis
|
| 289 |
+
- comprehensive: All analyses combined
|
| 290 |
+
|
| 291 |
+
Examples:
|
| 292 |
+
cidadao analyze temporal --period 2024
|
| 293 |
+
cidadao analyze supplier --org MIN_SAUDE --show-outliers
|
| 294 |
+
cidadao analyze comprehensive --period last-90-days --output json
|
| 295 |
+
"""
|
| 296 |
+
# Parse period
|
| 297 |
+
start_date, end_date = parse_period(period)
|
| 298 |
+
|
| 299 |
+
# Display start message
|
| 300 |
+
console.print(f"\n[bold blue]📊 Starting {analysis_type.value} Analysis[/bold blue]")
|
| 301 |
if period:
|
| 302 |
+
console.print(f"Period: [green]{period}[/green]")
|
| 303 |
+
if organizations:
|
| 304 |
+
console.print(f"Organizations: [cyan]{', '.join(organizations)}[/cyan]")
|
| 305 |
+
|
| 306 |
+
# Create request
|
| 307 |
+
request = AnalysisRequest(
|
| 308 |
+
analysis_type=analysis_type,
|
| 309 |
+
start_date=start_date,
|
| 310 |
+
end_date=end_date,
|
| 311 |
+
organizations=organizations or [],
|
| 312 |
+
suppliers=suppliers or [],
|
| 313 |
+
categories=categories or [],
|
| 314 |
+
regions=regions or [],
|
| 315 |
+
min_value=min_value,
|
| 316 |
+
max_value=max_value,
|
| 317 |
+
correlation_threshold=correlation_threshold
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
try:
|
| 321 |
+
# Execute analysis
|
| 322 |
+
with Progress(
|
| 323 |
+
SpinnerColumn(),
|
| 324 |
+
TextColumn("[progress.description]{task.description}"),
|
| 325 |
+
BarColumn(),
|
| 326 |
+
console=console
|
| 327 |
+
) as progress:
|
| 328 |
+
# Start analysis
|
| 329 |
+
task = progress.add_task("Initializing analysis...", total=100)
|
| 330 |
+
|
| 331 |
+
result_data = asyncio.run(
|
| 332 |
+
call_api(
|
| 333 |
+
"/api/v1/analysis/execute",
|
| 334 |
+
method="POST",
|
| 335 |
+
data=request.dict(),
|
| 336 |
+
auth_token=api_key
|
| 337 |
+
)
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
analysis_id = result_data.get("analysis_id")
|
| 341 |
+
progress.update(task, advance=20, description=f"Analysis ID: {analysis_id}")
|
| 342 |
+
|
| 343 |
+
# Poll for completion
|
| 344 |
+
while True:
|
| 345 |
+
status_data = asyncio.run(
|
| 346 |
+
call_api(
|
| 347 |
+
f"/api/v1/analysis/{analysis_id}",
|
| 348 |
+
auth_token=api_key
|
| 349 |
+
)
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
status = status_data.get("status", "unknown")
|
| 353 |
+
progress_pct = status_data.get("progress", 0) * 100
|
| 354 |
+
|
| 355 |
+
progress.update(
|
| 356 |
+
task,
|
| 357 |
+
completed=int(progress_pct),
|
| 358 |
+
description=f"Analyzing... ({status})"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
if status in ["completed", "failed"]:
|
| 362 |
+
break
|
| 363 |
+
|
| 364 |
+
asyncio.run(asyncio.sleep(1))
|
| 365 |
+
|
| 366 |
+
if status == "failed":
|
| 367 |
+
console.print(f"[red]❌ Analysis failed: {status_data.get('error', 'Unknown error')}[/red]")
|
| 368 |
+
raise typer.Exit(1)
|
| 369 |
+
|
| 370 |
+
# Create result object
|
| 371 |
+
result = AnalysisResult(
|
| 372 |
+
id=analysis_id,
|
| 373 |
+
analysis_type=analysis_type.value,
|
| 374 |
+
created_at=datetime.fromisoformat(status_data["created_at"]),
|
| 375 |
+
status=status,
|
| 376 |
+
summary=status_data.get("summary", {}),
|
| 377 |
+
patterns=status_data.get("patterns", []),
|
| 378 |
+
correlations=status_data.get("correlations", []),
|
| 379 |
+
trends=status_data.get("trends", []),
|
| 380 |
+
outliers=status_data.get("outliers", []),
|
| 381 |
+
statistics=status_data.get("statistics", {})
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
# Display results
|
| 385 |
+
console.print()
|
| 386 |
+
if output == OutputFormat.DASHBOARD:
|
| 387 |
+
display_dashboard(result)
|
| 388 |
+
if show_outliers and result.outliers:
|
| 389 |
+
console.print("\n[bold]🔴 Outliers Detail:[/bold]")
|
| 390 |
+
for outlier in result.outliers[:10]:
|
| 391 |
+
console.print(f" • {outlier.get('description', 'N/A')} [dim](score: {outlier.get('score', 0):.2f})[/dim]")
|
| 392 |
+
elif output == OutputFormat.TABLE:
|
| 393 |
+
display_table(result)
|
| 394 |
+
elif output == OutputFormat.JSON:
|
| 395 |
+
display_json(result)
|
| 396 |
+
elif output == OutputFormat.CSV:
|
| 397 |
+
if not save:
|
| 398 |
+
save = Path(f"analysis_{analysis_id}.csv")
|
| 399 |
+
export_csv(result, save)
|
| 400 |
+
|
| 401 |
+
# Save results if requested
|
| 402 |
+
if save and output != OutputFormat.CSV:
|
| 403 |
+
save_path = save.expanduser().resolve()
|
| 404 |
+
|
| 405 |
+
if output == OutputFormat.JSON:
|
| 406 |
+
import json
|
| 407 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
| 408 |
+
json.dump(result.dict(), f, indent=2, default=str, ensure_ascii=False)
|
| 409 |
+
else:
|
| 410 |
+
# Save as markdown
|
| 411 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
| 412 |
+
f.write(generate_analysis_report(result))
|
| 413 |
+
|
| 414 |
+
console.print(f"\n[green]✅ Results saved to: {save_path}[/green]")
|
| 415 |
+
|
| 416 |
+
# Summary
|
| 417 |
+
patterns_found = len(result.patterns)
|
| 418 |
+
if patterns_found > 0:
|
| 419 |
+
console.print(
|
| 420 |
+
f"\n[bold green]✅ Analysis complete: "
|
| 421 |
+
f"{patterns_found} patterns found[/bold green]"
|
| 422 |
+
)
|
| 423 |
+
else:
|
| 424 |
+
console.print("\n[yellow]⚠️ Analysis complete: No significant patterns found[/yellow]")
|
| 425 |
+
|
| 426 |
+
except Exception as e:
|
| 427 |
+
console.print(f"[red]❌ Error: {e}[/red]")
|
| 428 |
+
raise typer.Exit(1)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def parse_period(period: Optional[str]) -> tuple[Optional[datetime], Optional[datetime]]:
|
| 432 |
+
"""Parse period string into start and end dates."""
|
| 433 |
+
if not period:
|
| 434 |
+
return None, None
|
| 435 |
+
|
| 436 |
+
now = datetime.now()
|
| 437 |
+
|
| 438 |
+
# Year format (e.g., 2024)
|
| 439 |
+
if period.isdigit() and len(period) == 4:
|
| 440 |
+
year = int(period)
|
| 441 |
+
return datetime(year, 1, 1), datetime(year, 12, 31, 23, 59, 59)
|
| 442 |
+
|
| 443 |
+
# Quarter format (e.g., 2024-Q1)
|
| 444 |
+
if "-Q" in period:
|
| 445 |
+
year, quarter = period.split("-Q")
|
| 446 |
+
year = int(year)
|
| 447 |
+
quarter = int(quarter)
|
| 448 |
+
quarter_starts = {1: 1, 2: 4, 3: 7, 4: 10}
|
| 449 |
+
quarter_ends = {1: 3, 2: 6, 3: 9, 4: 12}
|
| 450 |
+
start_month = quarter_starts[quarter]
|
| 451 |
+
end_month = quarter_ends[quarter]
|
| 452 |
+
|
| 453 |
+
start = datetime(year, start_month, 1)
|
| 454 |
+
# Last day of quarter
|
| 455 |
+
if end_month == 12:
|
| 456 |
+
end = datetime(year, 12, 31, 23, 59, 59)
|
| 457 |
+
else:
|
| 458 |
+
end = datetime(year, end_month + 1, 1) - timedelta(seconds=1)
|
| 459 |
+
|
| 460 |
+
return start, end
|
| 461 |
+
|
| 462 |
+
# Relative periods
|
| 463 |
+
if period.startswith("last-"):
|
| 464 |
+
days_match = period.replace("last-", "").replace("-days", "")
|
| 465 |
+
if days_match.isdigit():
|
| 466 |
+
days = int(days_match)
|
| 467 |
+
return now - timedelta(days=days), now
|
| 468 |
+
|
| 469 |
+
# Default to current year
|
| 470 |
+
return datetime(now.year, 1, 1), now
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def generate_analysis_report(result: AnalysisResult) -> str:
|
| 474 |
+
"""Generate markdown analysis report."""
|
| 475 |
+
report = f"""# Cidadão.AI Analysis Report
|
| 476 |
+
|
| 477 |
+
**Analysis Type**: {result.analysis_type}
|
| 478 |
+
**Generated**: {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
| 479 |
+
|
| 480 |
+
## Summary
|
| 481 |
+
|
| 482 |
+
"""
|
| 483 |
+
|
| 484 |
+
# Add summary items
|
| 485 |
+
for key, value in result.summary.items():
|
| 486 |
+
report += f"- **{key.replace('_', ' ').title()}**: {value}\n"
|
| 487 |
+
|
| 488 |
+
# Patterns section
|
| 489 |
+
if result.patterns:
|
| 490 |
+
report += "\n## Patterns Detected\n\n"
|
| 491 |
+
for i, pattern in enumerate(result.patterns, 1):
|
| 492 |
+
report += f"### Pattern {i}: {pattern.get('name', 'Unknown')}\n\n"
|
| 493 |
+
report += f"- **Type**: {pattern.get('type', 'N/A')}\n"
|
| 494 |
+
report += f"- **Confidence**: {pattern.get('confidence', 0):.1%}\n"
|
| 495 |
+
report += f"- **Description**: {pattern.get('description', 'N/A')}\n"
|
| 496 |
+
report += f"- **Impact**: {pattern.get('impact', 'N/A')}\n\n"
|
| 497 |
+
|
| 498 |
+
# Correlations section
|
| 499 |
+
if result.correlations:
|
| 500 |
+
report += "\n## Correlations\n\n"
|
| 501 |
+
report += "| Variable 1 | Variable 2 | Correlation | p-value |\n"
|
| 502 |
+
report += "|------------|------------|-------------|--------|\n"
|
| 503 |
+
for corr in result.correlations:
|
| 504 |
+
report += f"| {corr.get('var1', 'N/A')} | {corr.get('var2', 'N/A')} | "
|
| 505 |
+
report += f"{corr.get('value', 0):.3f} | {corr.get('p_value', 0):.4f} |\n"
|
| 506 |
|
| 507 |
+
# Statistics
|
| 508 |
+
report += "\n## Statistics\n\n"
|
| 509 |
+
for key, value in result.statistics.items():
|
| 510 |
+
if isinstance(value, float):
|
| 511 |
+
report += f"- **{key.replace('_', ' ').title()}**: {value:,.2f}\n"
|
| 512 |
+
else:
|
| 513 |
+
report += f"- **{key.replace('_', ' ').title()}**: {value}\n"
|
| 514 |
|
| 515 |
+
report += "\n---\n*Analysis performed by Cidadão.AI - Multi-agent AI system for government transparency*"
|
|
|
|
| 516 |
|
| 517 |
+
return report
|
|
|
|
|
|
|
| 518 |
|
| 519 |
|
| 520 |
+
if __name__ == "__main__":
|
| 521 |
+
app()
|
|
@@ -1,41 +1,440 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
):
|
| 20 |
-
"""
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
"""
|
| 24 |
-
|
|
|
|
|
|
|
| 25 |
|
| 26 |
if org:
|
| 27 |
-
|
| 28 |
-
|
| 29 |
if year:
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
click.echo("⚠️ Funcionalidade em desenvolvimento")
|
| 37 |
-
click.echo("📋 Status: Implementação planejada para fase de produção")
|
| 38 |
|
| 39 |
|
| 40 |
-
if __name__ ==
|
| 41 |
-
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module: cli.commands.investigate
|
| 3 |
+
Description: Investigation command for CLI
|
| 4 |
+
Author: Anderson H. Silva
|
| 5 |
+
Date: 2025-01-25
|
| 6 |
+
License: Proprietary - All rights reserved
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
import sys
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Optional, List, Dict, Any
|
| 14 |
+
from enum import Enum
|
| 15 |
+
|
| 16 |
+
import typer
|
| 17 |
+
from rich.console import Console
|
| 18 |
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
| 19 |
+
from rich.table import Table
|
| 20 |
+
from rich.panel import Panel
|
| 21 |
+
from rich.text import Text
|
| 22 |
+
from rich.syntax import Syntax
|
| 23 |
+
import httpx
|
| 24 |
+
from pydantic import BaseModel, Field
|
| 25 |
+
|
| 26 |
+
# CLI app
|
| 27 |
+
app = typer.Typer(help="Execute investigations on government data")
|
| 28 |
+
console = Console()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class OutputFormat(str, Enum):
|
| 32 |
+
"""Output format options."""
|
| 33 |
+
JSON = "json"
|
| 34 |
+
MARKDOWN = "markdown"
|
| 35 |
+
HTML = "html"
|
| 36 |
+
TABLE = "table"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class InvestigationRequest(BaseModel):
|
| 40 |
+
"""Investigation request model."""
|
| 41 |
+
query: str = Field(description="Natural language query")
|
| 42 |
+
organization_code: Optional[str] = Field(None, description="Organization code")
|
| 43 |
+
year: Optional[int] = Field(None, description="Year filter")
|
| 44 |
+
threshold: float = Field(0.7, description="Anomaly detection threshold")
|
| 45 |
+
max_results: int = Field(100, description="Maximum results")
|
| 46 |
+
include_contracts: bool = Field(True, description="Include contract analysis")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class InvestigationResult(BaseModel):
|
| 50 |
+
"""Investigation result model."""
|
| 51 |
+
id: str
|
| 52 |
+
status: str
|
| 53 |
+
created_at: datetime
|
| 54 |
+
summary: Optional[str] = None
|
| 55 |
+
anomalies_count: int = 0
|
| 56 |
+
total_analyzed: int = 0
|
| 57 |
+
risk_score: float = 0.0
|
| 58 |
+
anomalies: List[Dict[str, Any]] = []
|
| 59 |
+
contracts: List[Dict[str, Any]] = []
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
async def call_api(
|
| 63 |
+
endpoint: str,
|
| 64 |
+
method: str = "GET",
|
| 65 |
+
data: Optional[Dict[str, Any]] = None,
|
| 66 |
+
params: Optional[Dict[str, Any]] = None,
|
| 67 |
+
auth_token: Optional[str] = None
|
| 68 |
+
) -> Dict[str, Any]:
|
| 69 |
+
"""Make API call to backend."""
|
| 70 |
+
# Get API URL from environment or use default
|
| 71 |
+
api_url = "http://localhost:8000"
|
| 72 |
+
|
| 73 |
+
headers = {
|
| 74 |
+
"Content-Type": "application/json",
|
| 75 |
+
"User-Agent": "Cidadao.AI-CLI/1.0"
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
if auth_token:
|
| 79 |
+
headers["Authorization"] = f"Bearer {auth_token}"
|
| 80 |
+
|
| 81 |
+
async with httpx.AsyncClient() as client:
|
| 82 |
+
response = await client.request(
|
| 83 |
+
method=method,
|
| 84 |
+
url=f"{api_url}{endpoint}",
|
| 85 |
+
headers=headers,
|
| 86 |
+
json=data,
|
| 87 |
+
params=params,
|
| 88 |
+
timeout=60.0
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
if response.status_code >= 400:
|
| 92 |
+
error_detail = response.json().get("detail", "Unknown error")
|
| 93 |
+
raise Exception(f"API Error: {error_detail}")
|
| 94 |
+
|
| 95 |
+
return response.json()
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def format_anomaly(anomaly: Dict[str, Any]) -> str:
|
| 99 |
+
"""Format anomaly for display."""
|
| 100 |
+
severity_color = "red" if anomaly.get("severity", 0) >= 0.8 else "yellow" if anomaly.get("severity", 0) >= 0.5 else "green"
|
| 101 |
+
|
| 102 |
+
return (
|
| 103 |
+
f"[{severity_color}]● Severidade: {anomaly.get('severity', 0):.2f}[/{severity_color}]\n"
|
| 104 |
+
f" Tipo: {anomaly.get('type', 'Unknown')}\n"
|
| 105 |
+
f" Descrição: {anomaly.get('description', 'N/A')}\n"
|
| 106 |
+
f" Explicação: {anomaly.get('explanation', 'N/A')}"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def display_results_table(result: InvestigationResult):
|
| 111 |
+
"""Display results in table format."""
|
| 112 |
+
# Summary panel
|
| 113 |
+
summary_text = f"""
|
| 114 |
+
[bold]Investigation ID:[/bold] {result.id}
|
| 115 |
+
[bold]Status:[/bold] {result.status}
|
| 116 |
+
[bold]Created:[/bold] {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
| 117 |
+
[bold]Risk Score:[/bold] [{get_risk_color(result.risk_score)}]{result.risk_score:.2f}[/]
|
| 118 |
+
[bold]Anomalies Found:[/bold] {result.anomalies_count} / {result.total_analyzed}
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
console.print(Panel(summary_text.strip(), title="📊 Investigation Summary", border_style="blue"))
|
| 122 |
+
|
| 123 |
+
# Anomalies table
|
| 124 |
+
if result.anomalies:
|
| 125 |
+
console.print("\n[bold]🚨 Anomalies Detected:[/bold]")
|
| 126 |
+
|
| 127 |
+
table = Table(show_header=True, header_style="bold magenta")
|
| 128 |
+
table.add_column("Type", style="dim", width=20)
|
| 129 |
+
table.add_column("Severity", justify="center")
|
| 130 |
+
table.add_column("Description", width=50)
|
| 131 |
+
table.add_column("Contract", style="dim")
|
| 132 |
+
|
| 133 |
+
for anomaly in result.anomalies[:10]: # Show first 10
|
| 134 |
+
severity = anomaly.get("severity", 0)
|
| 135 |
+
severity_color = get_risk_color(severity)
|
| 136 |
+
|
| 137 |
+
table.add_row(
|
| 138 |
+
anomaly.get("type", "Unknown"),
|
| 139 |
+
f"[{severity_color}]{severity:.2f}[/{severity_color}]",
|
| 140 |
+
anomaly.get("description", "N/A")[:50],
|
| 141 |
+
anomaly.get("contract_id", "N/A")
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
console.print(table)
|
| 145 |
+
|
| 146 |
+
if len(result.anomalies) > 10:
|
| 147 |
+
console.print(f"\n[dim]... and {len(result.anomalies) - 10} more anomalies[/dim]")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def display_results_markdown(result: InvestigationResult):
|
| 151 |
+
"""Display results in markdown format."""
|
| 152 |
+
markdown = f"""# Investigation Report
|
| 153 |
+
|
| 154 |
+
**ID**: {result.id}
|
| 155 |
+
**Status**: {result.status}
|
| 156 |
+
**Created**: {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
| 157 |
+
**Risk Score**: {result.risk_score:.2f}
|
| 158 |
+
**Anomalies**: {result.anomalies_count} found out of {result.total_analyzed} analyzed
|
| 159 |
+
|
| 160 |
+
## Summary
|
| 161 |
+
|
| 162 |
+
{result.summary or 'Investigation completed successfully.'}
|
| 163 |
+
|
| 164 |
+
## Anomalies Detected
|
| 165 |
+
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
for i, anomaly in enumerate(result.anomalies, 1):
|
| 169 |
+
markdown += f"""### Anomaly {i}
|
| 170 |
+
- **Type**: {anomaly.get('type', 'Unknown')}
|
| 171 |
+
- **Severity**: {anomaly.get('severity', 0):.2f}
|
| 172 |
+
- **Description**: {anomaly.get('description', 'N/A')}
|
| 173 |
+
- **Explanation**: {anomaly.get('explanation', 'N/A')}
|
| 174 |
+
- **Contract ID**: {anomaly.get('contract_id', 'N/A')}
|
| 175 |
+
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
syntax = Syntax(markdown, "markdown", theme="monokai", line_numbers=False)
|
| 179 |
+
console.print(syntax)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def display_results_json(result: InvestigationResult):
|
| 183 |
+
"""Display results in JSON format."""
|
| 184 |
+
import json
|
| 185 |
+
json_str = json.dumps(result.dict(), indent=2, default=str, ensure_ascii=False)
|
| 186 |
+
syntax = Syntax(json_str, "json", theme="monokai", line_numbers=False)
|
| 187 |
+
console.print(syntax)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def get_risk_color(risk_score: float) -> str:
|
| 191 |
+
"""Get color based on risk score."""
|
| 192 |
+
if risk_score >= 0.8:
|
| 193 |
+
return "red"
|
| 194 |
+
elif risk_score >= 0.5:
|
| 195 |
+
return "yellow"
|
| 196 |
+
else:
|
| 197 |
+
return "green"
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@app.command()
|
| 201 |
+
def investigate(
|
| 202 |
+
query: str = typer.Argument(help="Natural language description of what to investigate"),
|
| 203 |
+
org: Optional[str] = typer.Option(None, "--org", "-o", help="Organization code to focus investigation"),
|
| 204 |
+
year: Optional[int] = typer.Option(None, "--year", "-y", help="Year to investigate"),
|
| 205 |
+
threshold: float = typer.Option(0.7, "--threshold", "-t", min=0.0, max=1.0, help="Anomaly detection threshold"),
|
| 206 |
+
output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-f", help="Output format"),
|
| 207 |
+
max_results: int = typer.Option(100, "--max-results", "-m", help="Maximum number of results"),
|
| 208 |
+
no_contracts: bool = typer.Option(False, "--no-contracts", help="Exclude contract analysis"),
|
| 209 |
+
save: Optional[Path] = typer.Option(None, "--save", "-s", help="Save results to file"),
|
| 210 |
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="CIDADAO_API_KEY", help="API key for authentication"),
|
| 211 |
):
|
| 212 |
+
"""
|
| 213 |
+
🔍 Execute an investigation on government spending data.
|
| 214 |
|
| 215 |
+
This command starts a comprehensive investigation using multiple AI agents
|
| 216 |
+
to analyze government contracts and spending patterns for anomalies.
|
| 217 |
+
|
| 218 |
+
Examples:
|
| 219 |
+
cidadao investigate "contratos suspeitos em 2024"
|
| 220 |
+
cidadao investigate "gastos com educação" --org MIN_EDUCACAO --year 2024
|
| 221 |
+
cidadao investigate "anomalias em licitações" --threshold 0.8 --output json
|
| 222 |
"""
|
| 223 |
+
# Display start message
|
| 224 |
+
console.print(f"\n[bold blue]🔍 Starting Investigation[/bold blue]")
|
| 225 |
+
console.print(f"Query: [green]{query}[/green]")
|
| 226 |
|
| 227 |
if org:
|
| 228 |
+
console.print(f"Organization: [cyan]{org}[/cyan]")
|
|
|
|
| 229 |
if year:
|
| 230 |
+
console.print(f"Year: [cyan]{year}[/cyan]")
|
| 231 |
+
|
| 232 |
+
console.print(f"Anomaly threshold: [yellow]{threshold}[/yellow]")
|
| 233 |
+
console.print(f"Max results: [yellow]{max_results}[/yellow]")
|
| 234 |
+
console.print()
|
| 235 |
+
|
| 236 |
+
# Create investigation request
|
| 237 |
+
request = InvestigationRequest(
|
| 238 |
+
query=query,
|
| 239 |
+
organization_code=org,
|
| 240 |
+
year=year,
|
| 241 |
+
threshold=threshold,
|
| 242 |
+
max_results=max_results,
|
| 243 |
+
include_contracts=not no_contracts
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
try:
|
| 247 |
+
# Execute investigation with progress
|
| 248 |
+
with Progress(
|
| 249 |
+
SpinnerColumn(),
|
| 250 |
+
TextColumn("[progress.description]{task.description}"),
|
| 251 |
+
console=console
|
| 252 |
+
) as progress:
|
| 253 |
+
task = progress.add_task("Initializing investigation...", total=None)
|
| 254 |
+
|
| 255 |
+
# Start investigation
|
| 256 |
+
progress.update(task, description="Starting investigation...")
|
| 257 |
+
result_data = asyncio.run(
|
| 258 |
+
call_api(
|
| 259 |
+
"/api/v1/investigations/analyze",
|
| 260 |
+
method="POST",
|
| 261 |
+
data=request.dict(),
|
| 262 |
+
auth_token=api_key
|
| 263 |
+
)
|
| 264 |
+
)
|
| 265 |
+
|
| 266 |
+
investigation_id = result_data.get("investigation_id")
|
| 267 |
+
progress.update(task, description=f"Investigation ID: {investigation_id}")
|
| 268 |
+
|
| 269 |
+
# Poll for results
|
| 270 |
+
while True:
|
| 271 |
+
progress.update(task, description="Checking investigation status...")
|
| 272 |
+
|
| 273 |
+
status_data = asyncio.run(
|
| 274 |
+
call_api(
|
| 275 |
+
f"/api/v1/investigations/{investigation_id}",
|
| 276 |
+
auth_token=api_key
|
| 277 |
+
)
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
status = status_data.get("status", "unknown")
|
| 281 |
+
progress_pct = status_data.get("progress", 0) * 100
|
| 282 |
+
|
| 283 |
+
progress.update(
|
| 284 |
+
task,
|
| 285 |
+
description=f"Status: {status} ({progress_pct:.0f}%)"
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
if status in ["completed", "failed"]:
|
| 289 |
+
break
|
| 290 |
+
|
| 291 |
+
asyncio.run(asyncio.sleep(2))
|
| 292 |
+
|
| 293 |
+
# Process results
|
| 294 |
+
if status == "failed":
|
| 295 |
+
console.print(f"[red]❌ Investigation failed: {status_data.get('error', 'Unknown error')}[/red]")
|
| 296 |
+
raise typer.Exit(1)
|
| 297 |
+
|
| 298 |
+
# Create result object
|
| 299 |
+
result = InvestigationResult(
|
| 300 |
+
id=investigation_id,
|
| 301 |
+
status=status,
|
| 302 |
+
created_at=datetime.fromisoformat(status_data["created_at"]),
|
| 303 |
+
summary=status_data.get("summary"),
|
| 304 |
+
anomalies_count=len(status_data.get("anomalies", [])),
|
| 305 |
+
total_analyzed=status_data.get("total_analyzed", 0),
|
| 306 |
+
risk_score=status_data.get("risk_score", 0.0),
|
| 307 |
+
anomalies=status_data.get("anomalies", []),
|
| 308 |
+
contracts=status_data.get("contracts", [])
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
# Display results based on format
|
| 312 |
+
console.print()
|
| 313 |
+
|
| 314 |
+
if output == OutputFormat.TABLE:
|
| 315 |
+
display_results_table(result)
|
| 316 |
+
elif output == OutputFormat.MARKDOWN:
|
| 317 |
+
display_results_markdown(result)
|
| 318 |
+
elif output == OutputFormat.JSON:
|
| 319 |
+
display_results_json(result)
|
| 320 |
+
elif output == OutputFormat.HTML:
|
| 321 |
+
# For HTML, we'll convert markdown to HTML
|
| 322 |
+
console.print("[yellow]HTML output not yet implemented, showing markdown[/yellow]")
|
| 323 |
+
display_results_markdown(result)
|
| 324 |
+
|
| 325 |
+
# Save results if requested
|
| 326 |
+
if save:
|
| 327 |
+
save_path = save.expanduser().resolve()
|
| 328 |
+
|
| 329 |
+
if output == OutputFormat.JSON:
|
| 330 |
+
import json
|
| 331 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
| 332 |
+
json.dump(result.dict(), f, indent=2, default=str, ensure_ascii=False)
|
| 333 |
+
else:
|
| 334 |
+
# Save as text for other formats
|
| 335 |
+
with open(save_path, "w", encoding="utf-8") as f:
|
| 336 |
+
if output == OutputFormat.MARKDOWN:
|
| 337 |
+
f.write(generate_markdown_report(result))
|
| 338 |
+
else:
|
| 339 |
+
f.write(generate_text_report(result))
|
| 340 |
+
|
| 341 |
+
console.print(f"\n[green]✅ Results saved to: {save_path}[/green]")
|
| 342 |
+
|
| 343 |
+
# Summary message
|
| 344 |
+
if result.anomalies_count > 0:
|
| 345 |
+
risk_color = get_risk_color(result.risk_score)
|
| 346 |
+
console.print(
|
| 347 |
+
f"\n[bold {risk_color}]⚠️ Investigation complete: "
|
| 348 |
+
f"{result.anomalies_count} anomalies detected "
|
| 349 |
+
f"(risk score: {result.risk_score:.2f})[/bold {risk_color}]"
|
| 350 |
+
)
|
| 351 |
+
else:
|
| 352 |
+
console.print("\n[green]✅ Investigation complete: No anomalies detected[/green]")
|
| 353 |
+
|
| 354 |
+
except Exception as e:
|
| 355 |
+
console.print(f"[red]❌ Error: {e}[/red]")
|
| 356 |
+
raise typer.Exit(1)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def generate_markdown_report(result: InvestigationResult) -> str:
|
| 360 |
+
"""Generate full markdown report."""
|
| 361 |
+
report = f"""# Cidadão.AI Investigation Report
|
| 362 |
+
|
| 363 |
+
**Investigation ID**: {result.id}
|
| 364 |
+
**Date**: {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
| 365 |
+
**Status**: {result.status}
|
| 366 |
+
|
| 367 |
+
## Executive Summary
|
| 368 |
+
|
| 369 |
+
**Risk Score**: {result.risk_score:.2f}
|
| 370 |
+
**Anomalies Found**: {result.anomalies_count}
|
| 371 |
+
**Total Items Analyzed**: {result.total_analyzed}
|
| 372 |
+
|
| 373 |
+
{result.summary or 'Investigation completed successfully.'}
|
| 374 |
+
|
| 375 |
+
## Detailed Findings
|
| 376 |
+
|
| 377 |
+
### Anomalies Detected
|
| 378 |
+
|
| 379 |
+
"""
|
| 380 |
+
|
| 381 |
+
for i, anomaly in enumerate(result.anomalies, 1):
|
| 382 |
+
report += f"""#### Anomaly {i}
|
| 383 |
+
|
| 384 |
+
- **Type**: {anomaly.get('type', 'Unknown')}
|
| 385 |
+
- **Severity**: {anomaly.get('severity', 0):.2f}
|
| 386 |
+
- **Description**: {anomaly.get('description', 'N/A')}
|
| 387 |
+
- **Explanation**: {anomaly.get('explanation', 'N/A')}
|
| 388 |
+
- **Contract ID**: {anomaly.get('contract_id', 'N/A')}
|
| 389 |
+
- **Value**: R$ {anomaly.get('value', 0):,.2f}
|
| 390 |
+
|
| 391 |
+
"""
|
| 392 |
+
|
| 393 |
+
if result.contracts:
|
| 394 |
+
report += "\n### Related Contracts\n\n"
|
| 395 |
+
for contract in result.contracts[:10]:
|
| 396 |
+
report += f"""- **{contract.get('id', 'N/A')}**: {contract.get('description', 'N/A')}
|
| 397 |
+
- Value: R$ {contract.get('value', 0):,.2f}
|
| 398 |
+
- Supplier: {contract.get('supplier', 'N/A')}
|
| 399 |
+
|
| 400 |
+
"""
|
| 401 |
+
|
| 402 |
+
report += "\n---\n*Report generated by Cidadão.AI - Multi-agent AI system for government transparency*"
|
| 403 |
+
|
| 404 |
+
return report
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def generate_text_report(result: InvestigationResult) -> str:
|
| 408 |
+
"""Generate plain text report."""
|
| 409 |
+
report = f"""CIDADÃO.AI INVESTIGATION REPORT
|
| 410 |
+
================================
|
| 411 |
+
|
| 412 |
+
Investigation ID: {result.id}
|
| 413 |
+
Date: {result.created_at.strftime('%Y-%m-%d %H:%M:%S')}
|
| 414 |
+
Status: {result.status}
|
| 415 |
+
|
| 416 |
+
SUMMARY
|
| 417 |
+
-------
|
| 418 |
+
Risk Score: {result.risk_score:.2f}
|
| 419 |
+
Anomalies Found: {result.anomalies_count}
|
| 420 |
+
Total Analyzed: {result.total_analyzed}
|
| 421 |
+
|
| 422 |
+
{result.summary or 'Investigation completed successfully.'}
|
| 423 |
+
|
| 424 |
+
ANOMALIES
|
| 425 |
+
---------
|
| 426 |
+
"""
|
| 427 |
|
| 428 |
+
for i, anomaly in enumerate(result.anomalies, 1):
|
| 429 |
+
report += f"""
|
| 430 |
+
{i}. {anomaly.get('type', 'Unknown')} (Severity: {anomaly.get('severity', 0):.2f})
|
| 431 |
+
Description: {anomaly.get('description', 'N/A')}
|
| 432 |
+
Contract: {anomaly.get('contract_id', 'N/A')}
|
| 433 |
+
Value: R$ {anomaly.get('value', 0):,.2f}
|
| 434 |
+
"""
|
| 435 |
|
| 436 |
+
return report
|
|
|
|
|
|
|
| 437 |
|
| 438 |
|
| 439 |
+
if __name__ == "__main__":
|
| 440 |
+
app()
|
|
@@ -1,48 +1,487 @@
|
|
| 1 |
-
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
):
|
| 21 |
-
"""
|
|
|
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
"""
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
if
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
| 34 |
-
click.echo("📊 Incluindo gráficos e visualizações")
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
else:
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
-
if __name__ ==
|
| 48 |
-
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module: cli.commands.report
|
| 3 |
+
Description: Report generation command for CLI
|
| 4 |
+
Author: Anderson H. Silva
|
| 5 |
+
Date: 2025-01-25
|
| 6 |
+
License: Proprietary - All rights reserved
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Optional, List, Dict, Any
|
| 13 |
+
from enum import Enum
|
| 14 |
+
|
| 15 |
+
import typer
|
| 16 |
+
from rich.console import Console
|
| 17 |
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
| 18 |
+
from rich.table import Table
|
| 19 |
+
from rich.panel import Panel
|
| 20 |
+
from rich.syntax import Syntax
|
| 21 |
+
import httpx
|
| 22 |
+
from pydantic import BaseModel, Field
|
| 23 |
+
|
| 24 |
+
# CLI app
|
| 25 |
+
app = typer.Typer(help="Generate comprehensive reports from investigations and analyses")
|
| 26 |
+
console = Console()
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ReportType(str, Enum):
|
| 30 |
+
"""Report type options."""
|
| 31 |
+
INVESTIGATION = "investigation"
|
| 32 |
+
ANALYSIS = "analysis"
|
| 33 |
+
EXECUTIVE = "executive"
|
| 34 |
+
TECHNICAL = "technical"
|
| 35 |
+
AUDIT = "audit"
|
| 36 |
+
COMPLIANCE = "compliance"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class OutputFormat(str, Enum):
|
| 40 |
+
"""Output format options."""
|
| 41 |
+
PDF = "pdf"
|
| 42 |
+
MARKDOWN = "markdown"
|
| 43 |
+
HTML = "html"
|
| 44 |
+
EXCEL = "excel"
|
| 45 |
+
JSON = "json"
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class ReportRequest(BaseModel):
|
| 49 |
+
"""Report request model."""
|
| 50 |
+
report_type: ReportType
|
| 51 |
+
investigation_ids: List[str] = Field(default_factory=list)
|
| 52 |
+
analysis_ids: List[str] = Field(default_factory=list)
|
| 53 |
+
title: str
|
| 54 |
+
target_audience: str = "general"
|
| 55 |
+
include_visualizations: bool = True
|
| 56 |
+
include_raw_data: bool = False
|
| 57 |
+
time_range: Optional[Dict[str, str]] = None
|
| 58 |
+
language: str = "pt-BR"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
async def call_api(
|
| 62 |
+
endpoint: str,
|
| 63 |
+
method: str = "GET",
|
| 64 |
+
data: Optional[Dict[str, Any]] = None,
|
| 65 |
+
params: Optional[Dict[str, Any]] = None,
|
| 66 |
+
auth_token: Optional[str] = None
|
| 67 |
+
) -> Dict[str, Any]:
|
| 68 |
+
"""Make API call to backend."""
|
| 69 |
+
api_url = "http://localhost:8000"
|
| 70 |
+
|
| 71 |
+
headers = {
|
| 72 |
+
"Content-Type": "application/json",
|
| 73 |
+
"User-Agent": "Cidadao.AI-CLI/1.0"
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
if auth_token:
|
| 77 |
+
headers["Authorization"] = f"Bearer {auth_token}"
|
| 78 |
+
|
| 79 |
+
async with httpx.AsyncClient() as client:
|
| 80 |
+
response = await client.request(
|
| 81 |
+
method=method,
|
| 82 |
+
url=f"{api_url}{endpoint}",
|
| 83 |
+
headers=headers,
|
| 84 |
+
json=data,
|
| 85 |
+
params=params,
|
| 86 |
+
timeout=120.0
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
if response.status_code >= 400:
|
| 90 |
+
error_detail = response.json().get("detail", "Unknown error")
|
| 91 |
+
raise Exception(f"API Error: {error_detail}")
|
| 92 |
+
|
| 93 |
+
return response.json()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def display_report_preview(report_data: Dict[str, Any]):
|
| 97 |
+
"""Display report preview."""
|
| 98 |
+
console.print(
|
| 99 |
+
Panel(
|
| 100 |
+
f"[bold blue]📋 Report Generated[/bold blue]\n\n"
|
| 101 |
+
f"[bold]ID:[/bold] {report_data.get('report_id', 'N/A')}\n"
|
| 102 |
+
f"[bold]Type:[/bold] {report_data.get('report_type', 'N/A')}\n"
|
| 103 |
+
f"[bold]Title:[/bold] {report_data.get('title', 'N/A')}\n"
|
| 104 |
+
f"[bold]Status:[/bold] {report_data.get('status', 'N/A')}\n"
|
| 105 |
+
f"[bold]Word Count:[/bold] {report_data.get('word_count', 0):,}",
|
| 106 |
+
title="Report Summary",
|
| 107 |
+
border_style="blue"
|
| 108 |
+
)
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# Show first few lines of content
|
| 112 |
+
content = report_data.get('content', '')
|
| 113 |
+
if content:
|
| 114 |
+
lines = content.split('\n')
|
| 115 |
+
preview = '\n'.join(lines[:10])
|
| 116 |
+
if len(lines) > 10:
|
| 117 |
+
preview += "\n[dim]... (truncated)[/dim]"
|
| 118 |
+
|
| 119 |
+
console.print("\n[bold]Preview:[/bold]")
|
| 120 |
+
syntax = Syntax(preview, "markdown", theme="monokai", line_numbers=False)
|
| 121 |
+
console.print(syntax)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
async def download_report(report_id: str, format: str, save_path: Path, auth_token: Optional[str] = None):
|
| 125 |
+
"""Download report in specified format."""
|
| 126 |
+
# Get download URL
|
| 127 |
+
download_url = f"/api/v1/reports/{report_id}/download?format={format}"
|
| 128 |
+
|
| 129 |
+
# Download file
|
| 130 |
+
api_url = "http://localhost:8000"
|
| 131 |
+
headers = {"User-Agent": "Cidadao.AI-CLI/1.0"}
|
| 132 |
+
if auth_token:
|
| 133 |
+
headers["Authorization"] = f"Bearer {auth_token}"
|
| 134 |
+
|
| 135 |
+
async with httpx.AsyncClient() as client:
|
| 136 |
+
response = await client.get(
|
| 137 |
+
f"{api_url}{download_url}",
|
| 138 |
+
headers=headers,
|
| 139 |
+
timeout=60.0
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
if response.status_code >= 400:
|
| 143 |
+
raise Exception(f"Download failed: {response.text}")
|
| 144 |
+
|
| 145 |
+
# Save file
|
| 146 |
+
with open(save_path, "wb") as f:
|
| 147 |
+
f.write(response.content)
|
| 148 |
+
|
| 149 |
+
return len(response.content)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def get_file_extension(format: str) -> str:
|
| 153 |
+
"""Get file extension for format."""
|
| 154 |
+
extensions = {
|
| 155 |
+
"pdf": "pdf",
|
| 156 |
+
"markdown": "md",
|
| 157 |
+
"html": "html",
|
| 158 |
+
"excel": "xlsx",
|
| 159 |
+
"json": "json"
|
| 160 |
+
}
|
| 161 |
+
return extensions.get(format, "txt")
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@app.command()
|
| 165 |
+
def report(
|
| 166 |
+
report_type: ReportType = typer.Argument(help="Type of report to generate"),
|
| 167 |
+
investigations: Optional[List[str]] = typer.Option(None, "--investigation", "-i", help="Investigation IDs to include"),
|
| 168 |
+
analyses: Optional[List[str]] = typer.Option(None, "--analysis", "-a", help="Analysis IDs to include"),
|
| 169 |
+
title: str = typer.Option(None, "--title", "-t", help="Report title"),
|
| 170 |
+
audience: str = typer.Option("general", "--audience", help="Target audience: general, technical, executive, journalist"),
|
| 171 |
+
output: OutputFormat = typer.Option(OutputFormat.PDF, "--output", "-f", help="Output format"),
|
| 172 |
+
save_dir: Optional[Path] = typer.Option(None, "--save-dir", "-d", help="Directory to save report"),
|
| 173 |
+
filename: Optional[str] = typer.Option(None, "--filename", help="Custom filename (without extension)"),
|
| 174 |
+
include_data: bool = typer.Option(False, "--include-data", help="Include raw data appendix"),
|
| 175 |
+
no_visuals: bool = typer.Option(False, "--no-visuals", help="Exclude visualizations"),
|
| 176 |
+
language: str = typer.Option("pt-BR", "--language", "-l", help="Report language"),
|
| 177 |
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="CIDADAO_API_KEY", help="API key"),
|
| 178 |
):
|
| 179 |
+
"""
|
| 180 |
+
📋 Generate comprehensive reports from investigations and analyses.
|
| 181 |
|
| 182 |
+
This command creates professional reports combining investigation results,
|
| 183 |
+
analysis findings, and AI-generated insights in various formats.
|
| 184 |
+
|
| 185 |
+
Report Types:
|
| 186 |
+
- investigation: Detailed investigation findings
|
| 187 |
+
- analysis: Pattern and correlation analysis
|
| 188 |
+
- executive: High-level executive summary
|
| 189 |
+
- technical: In-depth technical report
|
| 190 |
+
- audit: Formal audit report
|
| 191 |
+
- compliance: Compliance verification report
|
| 192 |
+
|
| 193 |
+
Examples:
|
| 194 |
+
cidadao report investigation -i INV-001 INV-002 --output pdf
|
| 195 |
+
cidadao report executive -i INV-001 -a ANAL-001 --audience executive
|
| 196 |
+
cidadao report audit --investigation INV-001 --save-dir reports/
|
| 197 |
"""
|
| 198 |
+
# Validate inputs
|
| 199 |
+
if not investigations and not analyses:
|
| 200 |
+
console.print("[red]❌ Error: Must provide at least one investigation or analysis ID[/red]")
|
| 201 |
+
console.print("[dim]Use -i/--investigation or -a/--analysis options[/dim]")
|
| 202 |
+
raise typer.Exit(1)
|
| 203 |
+
|
| 204 |
+
# Generate title if not provided
|
| 205 |
+
if not title:
|
| 206 |
+
title = f"{report_type.value.title()} Report - {datetime.now().strftime('%Y-%m-%d')}"
|
| 207 |
|
| 208 |
+
# Display start message
|
| 209 |
+
console.print(f"\n[bold blue]📋 Generating {report_type.value} Report[/bold blue]")
|
| 210 |
+
console.print(f"Title: [green]{title}[/green]")
|
| 211 |
+
console.print(f"Format: [cyan]{output.value.upper()}[/cyan]")
|
| 212 |
+
console.print(f"Audience: [cyan]{audience}[/cyan]")
|
| 213 |
|
| 214 |
+
if investigations:
|
| 215 |
+
console.print(f"Investigations: [yellow]{', '.join(investigations)}[/yellow]")
|
| 216 |
+
if analyses:
|
| 217 |
+
console.print(f"Analyses: [yellow]{', '.join(analyses)}[/yellow]")
|
| 218 |
|
| 219 |
+
console.print()
|
|
|
|
| 220 |
|
| 221 |
+
# Create report request
|
| 222 |
+
request = ReportRequest(
|
| 223 |
+
report_type=report_type,
|
| 224 |
+
investigation_ids=investigations or [],
|
| 225 |
+
analysis_ids=analyses or [],
|
| 226 |
+
title=title,
|
| 227 |
+
target_audience=audience,
|
| 228 |
+
include_visualizations=not no_visuals,
|
| 229 |
+
include_raw_data=include_data,
|
| 230 |
+
language=language
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
# Convert output format to API format
|
| 234 |
+
api_format_map = {
|
| 235 |
+
OutputFormat.PDF: "pdf",
|
| 236 |
+
OutputFormat.MARKDOWN: "markdown",
|
| 237 |
+
OutputFormat.HTML: "html",
|
| 238 |
+
OutputFormat.EXCEL: "excel",
|
| 239 |
+
OutputFormat.JSON: "json"
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
# Generate report
|
| 244 |
+
with Progress(
|
| 245 |
+
SpinnerColumn(),
|
| 246 |
+
TextColumn("[progress.description]{task.description}"),
|
| 247 |
+
console=console
|
| 248 |
+
) as progress:
|
| 249 |
+
task = progress.add_task("Initializing report generation...", total=None)
|
| 250 |
+
|
| 251 |
+
# Start report generation
|
| 252 |
+
progress.update(task, description="Creating report...")
|
| 253 |
+
|
| 254 |
+
# Prepare request data
|
| 255 |
+
request_data = {
|
| 256 |
+
"report_type": request.report_type.value,
|
| 257 |
+
"title": request.title,
|
| 258 |
+
"target_audience": request.target_audience,
|
| 259 |
+
"output_format": api_format_map[output],
|
| 260 |
+
"investigation_ids": request.investigation_ids,
|
| 261 |
+
"analysis_ids": request.analysis_ids,
|
| 262 |
+
"data_sources": [], # Will be populated from investigations
|
| 263 |
+
"time_range": request.time_range or {},
|
| 264 |
+
"include_visualizations": request.include_visualizations,
|
| 265 |
+
"include_raw_data": request.include_raw_data
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
result_data = asyncio.run(
|
| 269 |
+
call_api(
|
| 270 |
+
"/api/v1/reports/generate",
|
| 271 |
+
method="POST",
|
| 272 |
+
data=request_data,
|
| 273 |
+
auth_token=api_key
|
| 274 |
+
)
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
report_id = result_data.get("report_id")
|
| 278 |
+
progress.update(task, description=f"Report ID: {report_id}")
|
| 279 |
+
|
| 280 |
+
# Poll for completion
|
| 281 |
+
while True:
|
| 282 |
+
progress.update(task, description="Generating report content...")
|
| 283 |
+
|
| 284 |
+
status_data = asyncio.run(
|
| 285 |
+
call_api(
|
| 286 |
+
f"/api/v1/reports/{report_id}/status",
|
| 287 |
+
auth_token=api_key
|
| 288 |
+
)
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
status = status_data.get("status", "unknown")
|
| 292 |
+
progress_pct = status_data.get("progress", 0)
|
| 293 |
+
current_phase = status_data.get("current_phase", "processing")
|
| 294 |
+
|
| 295 |
+
progress.update(
|
| 296 |
+
task,
|
| 297 |
+
description=f"Status: {current_phase} ({int(progress_pct * 100)}%)"
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
if status in ["completed", "failed"]:
|
| 301 |
+
break
|
| 302 |
+
|
| 303 |
+
asyncio.run(asyncio.sleep(2))
|
| 304 |
+
|
| 305 |
+
if status == "failed":
|
| 306 |
+
console.print(f"[red]❌ Report generation failed: {status_data.get('error_message', 'Unknown error')}[/red]")
|
| 307 |
+
raise typer.Exit(1)
|
| 308 |
+
|
| 309 |
+
# Get report data
|
| 310 |
+
report_data = asyncio.run(
|
| 311 |
+
call_api(
|
| 312 |
+
f"/api/v1/reports/{report_id}",
|
| 313 |
+
auth_token=api_key
|
| 314 |
+
)
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
# Display preview
|
| 318 |
+
display_report_preview(report_data)
|
| 319 |
+
|
| 320 |
+
# Save report if requested
|
| 321 |
+
if save_dir or filename:
|
| 322 |
+
# Determine save path
|
| 323 |
+
if not save_dir:
|
| 324 |
+
save_dir = Path.cwd()
|
| 325 |
+
else:
|
| 326 |
+
save_dir = save_dir.expanduser().resolve()
|
| 327 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 328 |
+
|
| 329 |
+
if not filename:
|
| 330 |
+
filename = f"{report_type.value}_report_{report_id}"
|
| 331 |
+
|
| 332 |
+
extension = get_file_extension(output.value)
|
| 333 |
+
save_path = save_dir / f"{filename}.{extension}"
|
| 334 |
+
|
| 335 |
+
# Download report
|
| 336 |
+
console.print(f"\n[yellow]Downloading report...[/yellow]")
|
| 337 |
+
|
| 338 |
+
file_size = asyncio.run(
|
| 339 |
+
download_report(
|
| 340 |
+
report_id,
|
| 341 |
+
api_format_map[output],
|
| 342 |
+
save_path,
|
| 343 |
+
auth_token=api_key
|
| 344 |
+
)
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
console.print(f"[green]✅ Report saved to: {save_path}[/green]")
|
| 348 |
+
console.print(f"[dim]File size: {file_size:,} bytes[/dim]")
|
| 349 |
+
else:
|
| 350 |
+
# Provide download URL
|
| 351 |
+
console.print(
|
| 352 |
+
f"\n[yellow]ℹ️ To download this report later:[/yellow]\n"
|
| 353 |
+
f"[dim]cidadao report download {report_id} --format {output.value}[/dim]"
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
# Summary
|
| 357 |
+
console.print(
|
| 358 |
+
f"\n[bold green]✅ Report generated successfully![/bold green]\n"
|
| 359 |
+
f"Report ID: {report_id}\n"
|
| 360 |
+
f"Word count: {report_data.get('word_count', 0):,}"
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
except Exception as e:
|
| 364 |
+
console.print(f"[red]❌ Error: {e}[/red]")
|
| 365 |
+
raise typer.Exit(1)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@app.command()
|
| 369 |
+
def download(
|
| 370 |
+
report_id: str = typer.Argument(help="Report ID to download"),
|
| 371 |
+
format: OutputFormat = typer.Option(OutputFormat.PDF, "--format", "-f", help="Download format"),
|
| 372 |
+
save_dir: Optional[Path] = typer.Option(None, "--save-dir", "-d", help="Directory to save report"),
|
| 373 |
+
filename: Optional[str] = typer.Option(None, "--filename", help="Custom filename (without extension)"),
|
| 374 |
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="CIDADAO_API_KEY", help="API key"),
|
| 375 |
+
):
|
| 376 |
+
"""
|
| 377 |
+
📥 Download an existing report.
|
| 378 |
+
|
| 379 |
+
Download a previously generated report in the specified format.
|
| 380 |
+
"""
|
| 381 |
+
# Determine save path
|
| 382 |
+
if not save_dir:
|
| 383 |
+
save_dir = Path.cwd()
|
| 384 |
else:
|
| 385 |
+
save_dir = save_dir.expanduser().resolve()
|
| 386 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 387 |
+
|
| 388 |
+
if not filename:
|
| 389 |
+
filename = f"report_{report_id}"
|
| 390 |
|
| 391 |
+
extension = get_file_extension(format.value)
|
| 392 |
+
save_path = save_dir / f"{filename}.{extension}"
|
| 393 |
+
|
| 394 |
+
console.print(f"[yellow]📥 Downloading report {report_id}...[/yellow]")
|
| 395 |
+
|
| 396 |
+
try:
|
| 397 |
+
# Convert format
|
| 398 |
+
api_format_map = {
|
| 399 |
+
OutputFormat.PDF: "pdf",
|
| 400 |
+
OutputFormat.MARKDOWN: "markdown",
|
| 401 |
+
OutputFormat.HTML: "html",
|
| 402 |
+
OutputFormat.EXCEL: "excel",
|
| 403 |
+
OutputFormat.JSON: "json"
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
file_size = asyncio.run(
|
| 407 |
+
download_report(
|
| 408 |
+
report_id,
|
| 409 |
+
api_format_map[format],
|
| 410 |
+
save_path,
|
| 411 |
+
auth_token=api_key
|
| 412 |
+
)
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
console.print(f"[green]✅ Report downloaded successfully![/green]")
|
| 416 |
+
console.print(f"[green]Saved to: {save_path}[/green]")
|
| 417 |
+
console.print(f"[dim]File size: {file_size:,} bytes[/dim]")
|
| 418 |
+
|
| 419 |
+
except Exception as e:
|
| 420 |
+
console.print(f"[red]❌ Error: {e}[/red]")
|
| 421 |
+
raise typer.Exit(1)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
@app.command()
|
| 425 |
+
def list(
|
| 426 |
+
report_type: Optional[ReportType] = typer.Option(None, "--type", "-t", help="Filter by report type"),
|
| 427 |
+
limit: int = typer.Option(10, "--limit", "-n", help="Number of reports to show"),
|
| 428 |
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="CIDADAO_API_KEY", help="API key"),
|
| 429 |
+
):
|
| 430 |
+
"""
|
| 431 |
+
📋 List your generated reports.
|
| 432 |
+
|
| 433 |
+
Show a list of previously generated reports.
|
| 434 |
+
"""
|
| 435 |
+
try:
|
| 436 |
+
# Build query params
|
| 437 |
+
params = {"limit": limit}
|
| 438 |
+
if report_type:
|
| 439 |
+
params["report_type"] = report_type.value
|
| 440 |
+
|
| 441 |
+
# Get reports
|
| 442 |
+
reports = asyncio.run(
|
| 443 |
+
call_api(
|
| 444 |
+
"/api/v1/reports/",
|
| 445 |
+
params=params,
|
| 446 |
+
auth_token=api_key
|
| 447 |
+
)
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
if not reports:
|
| 451 |
+
console.print("[yellow]No reports found[/yellow]")
|
| 452 |
+
return
|
| 453 |
+
|
| 454 |
+
# Display reports table
|
| 455 |
+
table = Table(title="Your Reports", show_header=True, header_style="bold magenta")
|
| 456 |
+
table.add_column("Report ID", style="dim")
|
| 457 |
+
table.add_column("Type")
|
| 458 |
+
table.add_column("Title", width=30)
|
| 459 |
+
table.add_column("Status")
|
| 460 |
+
table.add_column("Created", style="dim")
|
| 461 |
+
table.add_column("Words", justify="right")
|
| 462 |
+
|
| 463 |
+
for report in reports:
|
| 464 |
+
status = report.get("status", "unknown")
|
| 465 |
+
status_color = "green" if status == "completed" else "yellow" if status == "running" else "red"
|
| 466 |
+
|
| 467 |
+
table.add_row(
|
| 468 |
+
report.get("report_id", "N/A"),
|
| 469 |
+
report.get("report_type", "N/A"),
|
| 470 |
+
report.get("title", "N/A")[:30],
|
| 471 |
+
f"[{status_color}]{status}[/{status_color}]",
|
| 472 |
+
datetime.fromisoformat(report.get("started_at", "")).strftime("%Y-%m-%d %H:%M"),
|
| 473 |
+
f"{report.get('word_count', 0):,}" if report.get('word_count') else "-"
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
console.print(table)
|
| 477 |
+
|
| 478 |
+
console.print(f"\n[dim]Showing {len(reports)} most recent reports[/dim]")
|
| 479 |
+
console.print("[dim]Use 'cidadao report download <report_id>' to download a report[/dim]")
|
| 480 |
+
|
| 481 |
+
except Exception as e:
|
| 482 |
+
console.print(f"[red]❌ Error: {e}[/red]")
|
| 483 |
+
raise typer.Exit(1)
|
| 484 |
|
| 485 |
|
| 486 |
+
if __name__ == "__main__":
|
| 487 |
+
app()
|
|
@@ -25,9 +25,9 @@ from rich.panel import Panel
|
|
| 25 |
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 26 |
|
| 27 |
from src.cli.commands import (
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
watch_command,
|
| 32 |
)
|
| 33 |
from src.core.config import get_settings
|
|
@@ -45,9 +45,9 @@ app = typer.Typer(
|
|
| 45 |
console = Console()
|
| 46 |
|
| 47 |
# Add commands to main app
|
| 48 |
-
app.command("investigate", help="🔍 Executar investigações de anomalias em dados públicos")(
|
| 49 |
-
app.command("analyze", help="📊 Analisar padrões e correlações em dados governamentais")(
|
| 50 |
-
app.command("report", help="📋 Gerar relatórios detalhados de investigações")(
|
| 51 |
app.command("watch", help="👀 Monitorar dados em tempo real para anomalias")(watch_command)
|
| 52 |
|
| 53 |
|
|
|
|
| 25 |
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 26 |
|
| 27 |
from src.cli.commands import (
|
| 28 |
+
analyze,
|
| 29 |
+
investigate,
|
| 30 |
+
report,
|
| 31 |
watch_command,
|
| 32 |
)
|
| 33 |
from src.core.config import get_settings
|
|
|
|
| 45 |
console = Console()
|
| 46 |
|
| 47 |
# Add commands to main app
|
| 48 |
+
app.command("investigate", help="🔍 Executar investigações de anomalias em dados públicos")(investigate)
|
| 49 |
+
app.command("analyze", help="📊 Analisar padrões e correlações em dados governamentais")(analyze)
|
| 50 |
+
app.command("report", help="📋 Gerar relatórios detalhados de investigações")(report)
|
| 51 |
app.command("watch", help="👀 Monitorar dados em tempo real para anomalias")(watch_command)
|
| 52 |
|
| 53 |
|