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.
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/PKG-INFO +17 -2
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/README.md +16 -1
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/pyproject.toml +1 -1
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/__init__.py +1 -1
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/ask.py +1 -1
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/docs.py +2 -2
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/explain.py +3 -3
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/fs_search.py +4 -5
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/grep_cmd.py +3 -4
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/hybrid.py +4 -4
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/init.py +1 -1
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/lookup.py +2 -2
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/merged.py +3 -4
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/smart_search.py +3 -3
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/utils/output.py +131 -22
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/.gitignore +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/__init__.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/audit.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/auth.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/config.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/context.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/feedback.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/prompts.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/quota.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/read.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/search.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/commands/tokens.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/main.py +0 -0
- {vectorgov_cli-0.3.1 → vectorgov_cli-0.3.2}/src/vectorgov/cli/utils/__init__.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/vectorgov-cli/)
|
|
61
61
|
[](https://opensource.org/licenses/MIT)
|
|
62
62
|
|
|
63
|
-
|
|
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
|
[](https://pypi.org/project/vectorgov-cli/)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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))
|
|
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",
|
|
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
|
|
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))
|
|
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))
|
|
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",
|
|
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))
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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",
|
|
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))
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
321
|
+
if config_val and config_val.lower() in valid:
|
|
247
322
|
return config_val.lower()
|
|
248
323
|
except Exception:
|
|
249
324
|
pass
|
|
250
|
-
|
|
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("
|
|
290
|
-
table.add_column("
|
|
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
|
|
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(
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|