vectorgov-cli 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/PKG-INFO +17 -2
  2. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/README.md +16 -1
  3. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/pyproject.toml +1 -1
  4. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/__init__.py +1 -1
  5. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/ask.py +1 -1
  6. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/docs.py +2 -2
  7. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/explain.py +3 -3
  8. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/fs_search.py +4 -5
  9. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/grep_cmd.py +3 -4
  10. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/hybrid.py +4 -4
  11. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/init.py +1 -1
  12. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/lookup.py +2 -2
  13. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/merged.py +3 -4
  14. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/smart_search.py +3 -3
  15. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/utils/output.py +131 -22
  16. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/.gitignore +0 -0
  17. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/__init__.py +0 -0
  18. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/audit.py +0 -0
  19. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/auth.py +0 -0
  20. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/config.py +0 -0
  21. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/context.py +0 -0
  22. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/feedback.py +0 -0
  23. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/prompts.py +0 -0
  24. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/quota.py +0 -0
  25. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/read.py +0 -0
  26. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/search.py +0 -0
  27. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/tokens.py +0 -0
  28. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/main.py +0 -0
  29. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/utils/__init__.py +0 -0
  30. {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/utils/config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectorgov-cli
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: CLI para a API VectorGov - Busca semântica em legislação brasileira
5
5
  Project-URL: Homepage, https://vectorgov.io
6
6
  Project-URL: Documentation, https://vectorgov.io/documentacao
@@ -60,7 +60,17 @@ Cliente de linha de comando para a API VectorGov - Busca semântica em legislaç
60
60
  [![Python versions](https://img.shields.io/pypi/pyversions/vectorgov-cli.svg)](https://pypi.org/project/vectorgov-cli/)
61
61
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
62
62
 
63
- ## Para LLMs e agentes de IA (v0.2.3)
63
+ > **Novo em 0.3.2** zero truncamento de conteúdo em todos os formatos
64
+ > de saída + TTY detection automática: quando stdout não é terminal
65
+ > (pipe, LLM, CI/CD), o formato padrão vira `llm` automaticamente. A
66
+ > tabela do `search` ganhou coluna `Norma` para identificar de qual lei
67
+ > cada artigo veio. Veja o [CHANGELOG](CHANGELOG.md#032---2026-04-15).
68
+ >
69
+ > **Novo em 0.3.1** — parsing GNU-style: flags e argumentos funcionam em
70
+ > qualquer ordem (`search "ETP" --top-k 3` e `search --top-k 3 "ETP"`
71
+ > fazem a mesma coisa).
72
+
73
+ ## Para LLMs e agentes de IA (v0.3.2+)
64
74
 
65
75
  Se voce e um LLM ou agente de IA usando o CLI do VectorGov em sessoes
66
76
  de vibe coding, estas features foram projetadas para voce:
@@ -198,6 +208,11 @@ export VECTORGOV_API_KEY="vg_sua_chave"
198
208
 
199
209
  ## Uso
200
210
 
211
+ > **Ordem de argumentos**: o CLI aceita flags e argumentos posicionais em
212
+ > qualquer ordem (estilo GNU, igual a `git`, `curl`, `kubectl`, `npm`).
213
+ > `vectorgov search "ETP" --top-k 3` e `vectorgov search --top-k 3 "ETP"`
214
+ > produzem o mesmo resultado. Use o estilo que preferir.
215
+
201
216
  ### Busca
202
217
 
203
218
  ```bash
@@ -7,7 +7,17 @@ Cliente de linha de comando para a API VectorGov - Busca semântica em legislaç
7
7
  [![Python versions](https://img.shields.io/pypi/pyversions/vectorgov-cli.svg)](https://pypi.org/project/vectorgov-cli/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
 
10
- ## Para LLMs e agentes de IA (v0.2.3)
10
+ > **Novo em 0.3.2** zero truncamento de conteúdo em todos os formatos
11
+ > de saída + TTY detection automática: quando stdout não é terminal
12
+ > (pipe, LLM, CI/CD), o formato padrão vira `llm` automaticamente. A
13
+ > tabela do `search` ganhou coluna `Norma` para identificar de qual lei
14
+ > cada artigo veio. Veja o [CHANGELOG](CHANGELOG.md#032---2026-04-15).
15
+ >
16
+ > **Novo em 0.3.1** — parsing GNU-style: flags e argumentos funcionam em
17
+ > qualquer ordem (`search "ETP" --top-k 3` e `search --top-k 3 "ETP"`
18
+ > fazem a mesma coisa).
19
+
20
+ ## Para LLMs e agentes de IA (v0.3.2+)
11
21
 
12
22
  Se voce e um LLM ou agente de IA usando o CLI do VectorGov em sessoes
13
23
  de vibe coding, estas features foram projetadas para voce:
@@ -145,6 +155,11 @@ export VECTORGOV_API_KEY="vg_sua_chave"
145
155
 
146
156
  ## Uso
147
157
 
158
+ > **Ordem de argumentos**: o CLI aceita flags e argumentos posicionais em
159
+ > qualquer ordem (estilo GNU, igual a `git`, `curl`, `kubectl`, `npm`).
160
+ > `vectorgov search "ETP" --top-k 3` e `vectorgov search --top-k 3 "ETP"`
161
+ > produzem o mesmo resultado. Use o estilo que preferir.
162
+
148
163
  ### Busca
149
164
 
150
165
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vectorgov-cli"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "CLI para a API VectorGov - Busca semântica em legislação brasileira"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,7 +7,7 @@ Uso:
7
7
  vectorgov auth login
8
8
  """
9
9
 
10
- __version__ = "0.3.1"
10
+ __version__ = "0.3.2"
11
11
 
12
12
  from .main import app
13
13
 
@@ -148,7 +148,7 @@ def ask(
148
148
  for i, hit in enumerate(results.hits, 1):
149
149
  title = f"[bold]#{i}[/bold] Art. {hit.article_number} ({getattr(hit, 'document_id', None) or getattr(hit, 'source', '')})"
150
150
  console.print(Panel(
151
- hit.text[:500] + ("..." if len(hit.text) > 500 else ""),
151
+ hit.text,
152
152
  title=title,
153
153
  border_style="blue"
154
154
  ))
@@ -120,7 +120,7 @@ def list_docs(
120
120
  else:
121
121
  table = Table(title="Documentos Disponíveis")
122
122
  table.add_column("ID", style="cyan")
123
- table.add_column("Título")
123
+ table.add_column("Título", overflow="fold")
124
124
  table.add_column("Tipo", style="green")
125
125
  table.add_column("Ano", justify="right")
126
126
  table.add_column("Chunks", justify="right")
@@ -135,7 +135,7 @@ def list_docs(
135
135
  chunks = getattr(d, "chunks_count", 0)
136
136
  table.add_row(
137
137
  str(doc_id),
138
- titulo[:50] + "..." if len(str(titulo)) > 50 else str(titulo),
138
+ str(titulo),
139
139
  str(tipo),
140
140
  str(ano),
141
141
  str(chunks),
@@ -156,7 +156,7 @@ def explain(
156
156
  lines.append(main_text)
157
157
  if parent_text:
158
158
  lines.append("")
159
- lines.append(f"PARENT: {parent_text[:200]}")
159
+ lines.append(f"PARENT: {parent_text}")
160
160
  else:
161
161
  lines.append("")
162
162
  lines.append("PARENT: (nenhum -- dispositivo de primeiro nivel)")
@@ -197,10 +197,10 @@ def explain(
197
197
  console.print(f"\nStatus: [green]{status}[/green]")
198
198
  console.print(Panel(main_text, title=title, border_style="cyan"))
199
199
  if parent_text:
200
- console.print(f"[dim]Parent: {parent_text[:150]}...[/dim]")
200
+ console.print(Panel(parent_text, title="Parent", border_style="dim"))
201
201
  console.print(f"[dim]Filhos: {children_count} dispositivos[/dim]")
202
202
  if nota:
203
- console.print(f"\n[bold]Nota do especialista:[/bold] {nota[:200]}")
203
+ console.print(f"\n[bold]Nota do especialista:[/bold] {nota}")
204
204
  if evidence_url:
205
205
  console.print(f"[dim] Ver trecho: {evidence_url}[/dim]")
206
206
  if document_url:
@@ -115,7 +115,7 @@ def fs_search(
115
115
  {
116
116
  "source": getattr(h, "source", ""),
117
117
  "score": getattr(h, "score", 0),
118
- "text": getattr(h, "text", str(h))[:300],
118
+ "text": getattr(h, "text", str(h)),
119
119
  "breadcrumb": getattr(h, "breadcrumb", ""),
120
120
  }
121
121
  for h in hits
@@ -126,17 +126,16 @@ def fs_search(
126
126
  table = Table(title=f"Filesystem Search ({len(hits)} resultados)")
127
127
  table.add_column("#", style="dim", width=3)
128
128
  table.add_column("Fonte", style="cyan", width=30)
129
- table.add_column("Texto", width=65)
129
+ table.add_column("Texto", overflow="fold")
130
130
  table.add_column("Score", justify="right", width=6)
131
131
  table.add_column("Motivo", width=10)
132
132
  for i, h in enumerate(hits, 1):
133
133
  source = getattr(h, "source", getattr(h, "document_id", ""))
134
134
  score = getattr(h, "score", 0)
135
135
  text = getattr(h, "text", str(h))
136
- text = text[:160] + "..." if len(text) > 160 else text
137
136
  reason = getattr(h, "match_reason", "")
138
137
  table.add_row(
139
- str(i), source, text, f"{score:.2f}" if score else "-", reason[:10]
138
+ str(i), source, text, f"{score:.2f}" if score else "-", reason
140
139
  )
141
140
  console.print(table)
142
141
  else:
@@ -146,7 +145,7 @@ def fs_search(
146
145
  console.print(f"\n[cyan][{i}][/cyan] [bold]{source}[/bold]")
147
146
  if breadcrumb:
148
147
  console.print(f" [dim]{breadcrumb}[/dim]")
149
- console.print(f" {getattr(h, 'text', str(h))[:300]}")
148
+ console.print(f" {getattr(h, 'text', str(h))}")
150
149
  for line in render_evidence_lines_text(h):
151
150
  console.print(f"[dim]{line}[/dim]")
152
151
 
@@ -107,7 +107,7 @@ def grep(
107
107
  "request_id": extract_request_id(result),
108
108
  "hits": [
109
109
  {"source": getattr(h, "source", ""), "span_id": getattr(h, "span_id", ""),
110
- "text": getattr(h, "text", str(h))[:300]}
110
+ "text": getattr(h, "text", str(h))}
111
111
  for h in hits
112
112
  ],
113
113
  }
@@ -116,18 +116,17 @@ def grep(
116
116
  table = Table(title=f"grep: '{query}'")
117
117
  table.add_column("#", style="dim", width=3)
118
118
  table.add_column("Fonte", style="cyan", width=30)
119
- table.add_column("Trecho", width=80)
119
+ table.add_column("Trecho", overflow="fold")
120
120
  for i, h in enumerate(hits, 1):
121
121
  source = getattr(h, "source", getattr(h, "document_id", ""))
122
122
  text = getattr(h, "text", str(h))
123
- text = text[:200] + "..." if len(text) > 200 else text
124
123
  table.add_row(str(i), source, text)
125
124
  console.print(table)
126
125
  else:
127
126
  for i, h in enumerate(hits, 1):
128
127
  source = getattr(h, "source", getattr(h, "document_id", ""))
129
128
  console.print(f"[cyan][{i}][/cyan] [bold]{source}[/bold]")
130
- console.print(f" {getattr(h, 'text', str(h))[:300]}")
129
+ console.print(f" {getattr(h, 'text', str(h))}")
131
130
  for line in render_evidence_lines_text(h):
132
131
  console.print(f"[dim]{line}[/dim]")
133
132
  console.print()
@@ -141,7 +141,7 @@ def hybrid(
141
141
  {
142
142
  "source": getattr(h, "source", ""),
143
143
  "score": getattr(h, "score", 0),
144
- "text": h.text[:300],
144
+ "text": h.text,
145
145
  "graph": getattr(h, "is_graph_expanded", False),
146
146
  }
147
147
  for h in hits
@@ -151,13 +151,13 @@ def hybrid(
151
151
  table = Table(title=f"Busca Híbrida ({len(hits)} resultados)")
152
152
  table.add_column("#", style="dim", width=3)
153
153
  table.add_column("Fonte", style="cyan", width=30)
154
- table.add_column("Texto", width=70)
154
+ table.add_column("Texto", overflow="fold")
155
155
  table.add_column("Score", justify="right", width=6)
156
156
  table.add_column("Grafo", width=5)
157
157
  for i, h in enumerate(hits, 1):
158
158
  source = getattr(h, "source", getattr(h, "document_id", ""))
159
159
  score = getattr(h, "score", 0)
160
- text = h.text[:180] + "..." if len(h.text) > 180 else h.text
160
+ text = h.text
161
161
  graph = "+" if getattr(h, "is_graph_expanded", False) else ""
162
162
  table.add_row(
163
163
  str(i), source, text, f"{score:.2f}" if score else "-", graph
@@ -172,7 +172,7 @@ def hybrid(
172
172
  else ""
173
173
  )
174
174
  console.print(f"\n[cyan][{i}][/cyan] [bold]{source}[/bold]{graph_tag}")
175
- console.print(f" {h.text[:300]}")
175
+ console.print(f" {h.text}")
176
176
  for line in render_evidence_lines_text(h):
177
177
  console.print(f"[dim]{line}[/dim]")
178
178
 
@@ -127,7 +127,7 @@ results = vg.search("O que é ETP?", top_k=5)
127
127
  print(f"Encontrados: {results.total} resultados")
128
128
 
129
129
  for hit in results:
130
- print(f" [{getattr(hit, 'document_id', None) or getattr(hit, 'source', '')}] Art. {hit.article_number}: {hit.text[:100]}...")
130
+ print(f" [{getattr(hit, 'document_id', None) or getattr(hit, 'source', '')}] Art. {hit.article_number}: {hit.text}")
131
131
 
132
132
  # Contexto para LLM
133
133
  context = results.to_context()
@@ -111,7 +111,7 @@ def lookup( # noqa: C901
111
111
  if output == "llm":
112
112
  print(f"[{i+1}/{len(batch_results)}] {ref_str} ({status})")
113
113
  if text:
114
- print(text[:500])
114
+ print(text)
115
115
  if ev:
116
116
  print(f"EVIDENCE: {ev}")
117
117
  if doc_url:
@@ -121,7 +121,7 @@ def lookup( # noqa: C901
121
121
  else:
122
122
  console.print(f"\n[bold cyan][{i+1}] {ref_str}[/bold cyan] [{status}]")
123
123
  if text:
124
- console.print(text[:300])
124
+ console.print(text)
125
125
  if ev:
126
126
  console.print(f"[dim] Ver trecho: {ev}[/dim]")
127
127
  if doc_url:
@@ -141,7 +141,7 @@ def merged(
141
141
  "request_id": extract_request_id(result),
142
142
  "hits": [
143
143
  {"source": getattr(h, "source", ""), "score": getattr(h, "score", 0),
144
- "text": getattr(h, "text", str(h))[:300], "path": getattr(h, "path", "")}
144
+ "text": getattr(h, "text", str(h)), "path": getattr(h, "path", "")}
145
145
  for h in hits
146
146
  ],
147
147
  }
@@ -150,20 +150,19 @@ def merged(
150
150
  table = Table(title=f"Merged Search ({len(hits)} resultados)")
151
151
  table.add_column("#", style="dim", width=3)
152
152
  table.add_column("Fonte", style="cyan", width=30)
153
- table.add_column("Texto", width=70)
153
+ table.add_column("Texto", overflow="fold")
154
154
  table.add_column("Score", justify="right", width=6)
155
155
  for i, h in enumerate(hits, 1):
156
156
  source = getattr(h, "source", getattr(h, "document_id", ""))
157
157
  score = getattr(h, "score", 0)
158
158
  text = getattr(h, "text", str(h))
159
- text = text[:180] + "..." if len(text) > 180 else text
160
159
  table.add_row(str(i), source, text, f"{score:.2f}" if score else "-")
161
160
  console.print(table)
162
161
  else:
163
162
  for i, h in enumerate(hits, 1):
164
163
  source = getattr(h, "source", getattr(h, "document_id", ""))
165
164
  console.print(f"\n[cyan][{i}][/cyan] [bold]{source}[/bold]")
166
- console.print(f" {getattr(h, 'text', str(h))[:300]}")
165
+ console.print(f" {getattr(h, 'text', str(h))}")
167
166
  for line in render_evidence_lines_text(h):
168
167
  console.print(f"[dim]{line}[/dim]")
169
168
 
@@ -124,12 +124,12 @@ def smart_search(
124
124
  table = Table(title=f"Resultados ({result.total})")
125
125
  table.add_column("#", style="dim", width=3)
126
126
  table.add_column("Fonte", style="cyan", width=30)
127
- table.add_column("Texto", width=80)
127
+ table.add_column("Texto", overflow="fold")
128
128
  table.add_column("Score", justify="right", width=6)
129
129
  for i, h in enumerate(result.hits, 1):
130
130
  source = getattr(h, "source", getattr(h, "document_id", ""))
131
131
  score = getattr(h, "score", 0)
132
- text = h.text[:200] + "..." if len(h.text) > 200 else h.text
132
+ text = h.text
133
133
  table.add_row(
134
134
  str(i), source, text, f"{score:.2f}" if score else "-"
135
135
  )
@@ -138,7 +138,7 @@ def smart_search(
138
138
  for i, h in enumerate(result.hits, 1):
139
139
  source = getattr(h, "source", getattr(h, "document_id", ""))
140
140
  console.print(f"\n[cyan][{i}][/cyan] [bold]{source}[/bold]")
141
- console.print(f" {h.text[:300]}")
141
+ console.print(f" {h.text}")
142
142
  for line in render_evidence_lines_text(h):
143
143
  console.print(f"[dim]{line}[/dim]")
144
144
 
@@ -4,6 +4,7 @@ Utilitários de formatação de saída do CLI VectorGov.
4
4
 
5
5
  import json
6
6
  import os
7
+ import sys
7
8
  from enum import Enum
8
9
  from typing import Any, Dict, List, Optional
9
10
 
@@ -15,6 +16,57 @@ from rich.panel import Panel
15
16
  BASE_URL = "https://vectorgov.io"
16
17
 
17
18
 
19
+ # ─── Formatação de document_id para exibição humana ─────────────────────────
20
+
21
+ _TIPO_LABEL = {
22
+ "LEI": "Lei",
23
+ "DECRETO": "Decreto",
24
+ "IN": "IN",
25
+ "PORTARIA": "Portaria",
26
+ "AC": "Acórdão",
27
+ "LC": "LC",
28
+ "MP": "MP",
29
+ "EC": "EC",
30
+ "RES": "Resolução",
31
+ "RESOLUCAO": "Resolução",
32
+ }
33
+
34
+
35
+ def format_document_label(document_id: Optional[str]) -> str:
36
+ """Formata document_id para exibição humana.
37
+
38
+ Exemplos:
39
+ 'LEI-14133-2021' -> 'Lei 14.133/2021'
40
+ 'IN-65-2021' -> 'IN 65/2021'
41
+ 'DECRETO-10947-2022' -> 'Decreto 10.947/2022'
42
+ 'PORTARIA-938-2022' -> 'Portaria 938/2022'
43
+ 'AC-1852-2020-P' -> 'Acórdão 1.852/2020-P'
44
+
45
+ Fallback: retorna o document_id cru se não conseguir parsear.
46
+ """
47
+ if not document_id:
48
+ return "-"
49
+ parts = document_id.split("-")
50
+ if len(parts) < 3:
51
+ return document_id
52
+ tipo = _TIPO_LABEL.get(parts[0].upper(), parts[0])
53
+ num = parts[1]
54
+ ano = parts[2]
55
+ suffix = "-" + "-".join(parts[3:]) if len(parts) > 3 else ""
56
+ if num.isdigit() and len(num) >= 4:
57
+ num_fmt = f"{int(num):,}".replace(",", ".")
58
+ else:
59
+ num_fmt = num
60
+ return f"{tipo} {num_fmt}/{ano}{suffix}"
61
+
62
+
63
+ def get_hit_document_id(hit: Any) -> Optional[str]:
64
+ """Extrai document_id (ou source como fallback) de um hit."""
65
+ if isinstance(hit, dict):
66
+ return hit.get("document_id") or hit.get("source")
67
+ return getattr(hit, "document_id", None) or getattr(hit, "source", None)
68
+
69
+
18
70
  class OutputFormat(str, Enum):
19
71
  """Formatos de saída suportados."""
20
72
  table = "table"
@@ -161,7 +213,11 @@ def _hit_get(hit, key: str, default: Any = "") -> Any:
161
213
 
162
214
 
163
215
  def render_llm_output(hits: list, query: str, metadata: Optional[dict] = None) -> str:
164
- """Output otimizado para consumo por LLMs. Texto puro, sem formatacao."""
216
+ """Output otimizado para consumo por LLMs. Texto puro, sem formatacao.
217
+
218
+ Sem truncamento — retorna o texto integral de cada hit. Se o usuario
219
+ quer resposta menor, use --top-k N.
220
+ """
165
221
  lines = []
166
222
  total = len(hits)
167
223
  for i, hit in enumerate(hits, 1):
@@ -182,7 +238,7 @@ def render_llm_output(hits: list, query: str, metadata: Optional[dict] = None) -
182
238
 
183
239
  lines.append(header)
184
240
  if text:
185
- lines.append(text[:2000])
241
+ lines.append(text)
186
242
 
187
243
  links = get_evidence_links(hit)
188
244
  if links["evidence_url"]:
@@ -228,26 +284,52 @@ def render_llm_output(hits: list, query: str, metadata: Optional[dict] = None) -
228
284
 
229
285
  # ─── Resolução de formato de output ─────────────────────────────────────────
230
286
 
231
- def resolve_output_format(explicit: str = "table", is_raw: bool = False) -> str:
232
- """Resolve formato de output com fallback: flag > env var > config > default.
233
-
234
- Se is_raw=True, retorna "raw" (bypass tudo).
287
+ def resolve_output_format(
288
+ explicit: str = "table",
289
+ is_raw: bool = False,
290
+ command_default: str = "table",
291
+ ) -> str:
292
+ """Resolve formato de output com fallback em cascata.
293
+
294
+ Precedência:
295
+ 1. is_raw=True -> "raw"
296
+ 2. flag --output != default do cmd -> respeita flag
297
+ 3. env VECTORGOV_OUTPUT -> respeita env
298
+ 4. config default_output -> respeita config
299
+ 5. stdout não é TTY (pipe/LLM/CI) -> "llm" (GNU-style)
300
+ 6. default do comando -> fallback final
301
+
302
+ A detecção de TTY (passo 5) faz o CLI ser LLM-friendly out of the box:
303
+ quando um agente ou script executa `vectorgov search "..." | something`,
304
+ recebe texto integral no formato llm em vez da tabela truncada.
235
305
  """
306
+ valid = ("table", "json", "text", "markdown", "llm", "raw")
307
+
236
308
  if is_raw:
237
309
  return "raw"
238
- if explicit and explicit != "table":
310
+
311
+ if explicit and explicit != command_default and explicit in valid:
239
312
  return explicit
313
+
240
314
  env_val = os.environ.get("VECTORGOV_OUTPUT", "").lower().strip()
241
- if env_val and env_val in ("table", "json", "text", "markdown", "llm", "raw"):
315
+ if env_val and env_val in valid:
242
316
  return env_val
317
+
243
318
  try:
244
319
  from ..utils.config import ConfigManager
245
320
  config_val = ConfigManager().get("default_output")
246
- if config_val and config_val.lower() in ("table", "json", "text", "markdown", "llm", "raw"):
321
+ if config_val and config_val.lower() in valid:
247
322
  return config_val.lower()
248
323
  except Exception:
249
324
  pass
250
- return explicit or "table"
325
+
326
+ try:
327
+ if not sys.stdout.isatty():
328
+ return "llm"
329
+ except (AttributeError, ValueError):
330
+ pass
331
+
332
+ return explicit or command_default
251
333
 
252
334
 
253
335
  # ─── Formatador principal ────────────────────────────────────────────────────
@@ -283,27 +365,45 @@ def format_search_results(console: Console, results: Any, output_format: OutputF
283
365
  console.print(f"[dim]Total: {results.total} | Latência: {results.latency_ms:.0f}ms | Cache: {'Sim' if results.cached else 'Não'}[/dim]")
284
366
  console.print()
285
367
 
286
- # Tabela
368
+ # Tabela — sem truncamento de texto. Rich faz word-wrap via overflow="fold"
369
+ # na coluna Texto. Se o resultado for muito alto visualmente, use --top-k N.
287
370
  table = Table(show_header=True, header_style="bold cyan")
288
371
  table.add_column("#", justify="right", style="dim", width=3)
289
- table.add_column("Artigo", style="green", width=10)
290
- table.add_column("Texto", width=55)
372
+ table.add_column("Norma", style="cyan", width=22)
373
+ table.add_column("Artigo", style="green", width=8)
374
+ table.add_column("Texto", overflow="fold")
291
375
  table.add_column("Score", justify="right", width=7)
292
376
  table.add_column("Evidência", width=14)
293
377
 
294
378
  for i, hit in enumerate(results.hits, 1):
379
+ doc_id = get_hit_document_id(hit)
380
+ norma = format_document_label(doc_id)
295
381
  article = getattr(hit, "article_number", "-") or "-"
296
- text = hit.text[:95] + "..." if len(hit.text) > 95 else hit.text
297
- text = text.replace("\n", " ")
382
+ text = hit.text # sem truncamento, sem replace \n dado integral
298
383
  evidence_cell = render_evidence_cell_table(hit)
299
- table.add_row(str(i), str(article), text, f"{hit.score:.3f}", evidence_cell)
384
+ table.add_row(
385
+ str(i),
386
+ norma,
387
+ str(article),
388
+ text,
389
+ f"{hit.score:.3f}",
390
+ evidence_cell,
391
+ )
300
392
 
301
393
  console.print(table)
302
394
 
303
- # Footer com dica sobre evidência
304
- first_links = get_evidence_links(results.hits[0]) if results.hits else {"document_url": None}
305
- if first_links.get("document_url"):
306
- console.print(f"\n[dim]PDF do primeiro resultado: {first_links['document_url']}[/dim]")
395
+ # Footer: lista fontes únicas (substitui "PDF do primeiro resultado")
396
+ unique_docs: List[str] = []
397
+ seen: set = set()
398
+ for h in results.hits:
399
+ doc_id = get_hit_document_id(h)
400
+ if doc_id and doc_id not in seen:
401
+ seen.add(doc_id)
402
+ unique_docs.append(format_document_label(doc_id))
403
+ if unique_docs:
404
+ console.print(
405
+ f"\n[dim]Fontes ({len(unique_docs)}): {', '.join(unique_docs)}[/dim]"
406
+ )
307
407
  console.print(f"[dim]Query ID: {results.query_id}[/dim]")
308
408
  if request_id:
309
409
  console.print(f"[dim]Request ID: {request_id}[/dim]")
@@ -341,10 +441,19 @@ def format_search_results(console: Console, results: Any, output_format: OutputF
341
441
 
342
442
  for i, hit in enumerate(results.hits, 1):
343
443
  article = getattr(hit, "article_number", None)
344
- header = f"[{i}] Art. {article}" if article else f"[{i}]"
444
+ doc_id = get_hit_document_id(hit)
445
+ norma = format_document_label(doc_id)
446
+ if article and doc_id:
447
+ header = f"[{i}] {norma}, Art. {article}"
448
+ elif article:
449
+ header = f"[{i}] Art. {article}"
450
+ elif doc_id:
451
+ header = f"[{i}] {norma}"
452
+ else:
453
+ header = f"[{i}]"
345
454
 
346
455
  console.print(f"\n[bold cyan]{header}[/bold cyan] (score: {hit.score:.3f})")
347
- console.print(hit.text[:300] + "..." if len(hit.text) > 300 else hit.text)
456
+ console.print(hit.text) # sem truncamento
348
457
  # Links de evidência
349
458
  for line in render_evidence_lines_text(hit):
350
459
  console.print(f"[dim]{line}[/dim]")
File without changes