@danhachuel/leadmaster 0.0.0-stage → 0.1.1
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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/backend/main.py +466 -0
- package/backend/requirements.txt +4 -0
- package/backend/services/__init__.py +1 -0
- package/backend/services/agents.py +235 -0
- package/backend/services/crm_agent.py +175 -0
- package/backend/services/extractors.py +322 -0
- package/backend/services/llm.py +69 -0
- package/backend/services/storage.py +440 -0
- package/frontend/dist/assets/fa-brands-400-D1LuMI3I.ttf +0 -0
- package/frontend/dist/assets/fa-brands-400-D_cYUPeE.woff2 +0 -0
- package/frontend/dist/assets/fa-regular-400-BjRzuEpd.woff2 +0 -0
- package/frontend/dist/assets/fa-regular-400-DZaxPHgR.ttf +0 -0
- package/frontend/dist/assets/fa-solid-900-CTAAxXor.woff2 +0 -0
- package/frontend/dist/assets/fa-solid-900-D0aA9rwL.ttf +0 -0
- package/frontend/dist/assets/fa-v4compatibility-C9RhG_FT.woff2 +0 -0
- package/frontend/dist/assets/fa-v4compatibility-CCth-dXg.ttf +0 -0
- package/frontend/dist/assets/index-CFQHp6yf.css +5 -0
- package/frontend/dist/assets/index-CPq53JWx.js +90 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/index.html +14 -0
- package/package.json +47 -4
- package/scripts/cli.mjs +512 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Extratores de leads: Google Maps, Instagram e CNPJ.
|
|
2
|
+
|
|
3
|
+
Princípios:
|
|
4
|
+
- Nenhuma extração importa automaticamente: o frontend pré-visualiza e o
|
|
5
|
+
usuário confirma a importação.
|
|
6
|
+
- Toda integração externa falha graciosamente com instruções acionáveis.
|
|
7
|
+
- Os comandos de CLI usam templates de argumentos configuráveis, porque as
|
|
8
|
+
flags dos projetos upstream podem variar entre versões.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import csv
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from . import storage
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExtratorNaoConfigurado(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExtratorErro(Exception):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _conf() -> dict:
|
|
31
|
+
return storage.get_config()["extratores"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _novo_lead_parcial() -> dict:
|
|
35
|
+
return {"tags": [], "observacoes": "", "score": 50}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# Google Maps (gosom/google-maps-scraper via CLI)
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def extrair_google_maps(palavra_chave: str, localizacao: str, max_resultados: int = 100) -> dict:
|
|
43
|
+
conf = _conf()
|
|
44
|
+
cli = conf.get("google_maps_cli", "")
|
|
45
|
+
if not cli or not Path(cli).exists():
|
|
46
|
+
raise ExtratorNaoConfigurado(
|
|
47
|
+
"Google Maps Scraper não configurado. Instale https://github.com/gosom/google-maps-scraper "
|
|
48
|
+
"e informe o caminho do binário em Ferramentas → Extratores."
|
|
49
|
+
)
|
|
50
|
+
with tempfile.TemporaryDirectory(prefix="leadmaster-maps-") as tmp:
|
|
51
|
+
outdir = Path(tmp)
|
|
52
|
+
args_template = conf.get(
|
|
53
|
+
"google_maps_args",
|
|
54
|
+
'--kw "{palavra_chave}" --loc "{localizacao}" --lang pt --email --max {max_resultados} --outdir "{outdir}"',
|
|
55
|
+
)
|
|
56
|
+
cmd = [cli] + _formatar_args(
|
|
57
|
+
args_template,
|
|
58
|
+
palavra_chave=palavra_chave,
|
|
59
|
+
localizacao=localizacao,
|
|
60
|
+
max_resultados=max_resultados,
|
|
61
|
+
outdir=str(outdir),
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
resultado = subprocess.run(cmd, capture_output=True, text=True, timeout=1800, shell=False)
|
|
65
|
+
except FileNotFoundError as e:
|
|
66
|
+
raise ExtratorNaoConfigurado(f"Não foi possível executar o CLI do Google Maps Scraper: {e}") from e
|
|
67
|
+
if resultado.returncode != 0:
|
|
68
|
+
raise ExtratorErro(
|
|
69
|
+
f"Google Maps Scraper terminou com código {resultado.returncode}: {resultado.stderr[-400:]}. "
|
|
70
|
+
"Confira as flags do template em Ferramentas → Extratores."
|
|
71
|
+
)
|
|
72
|
+
leads = _parse_resultados_diretorio(outdir, fonte="Google Maps")
|
|
73
|
+
return {"leads": leads, "comando": " ".join(cmd[:4]) + " ..."}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Instagram (lucasoal/igmapper via CLI)
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def extrair_instagram(profile_url: str) -> dict:
|
|
81
|
+
conf = _conf()
|
|
82
|
+
cli = conf.get("igmapper_cli", "")
|
|
83
|
+
if not cli or not Path(cli).exists():
|
|
84
|
+
raise ExtratorNaoConfigurado(
|
|
85
|
+
"igmapper não configurado. Instale https://github.com/lucasoal/igmapper "
|
|
86
|
+
"e informe o caminho em Ferramentas → Extratores."
|
|
87
|
+
)
|
|
88
|
+
with tempfile.TemporaryDirectory(prefix="leadmaster-ig-") as tmp:
|
|
89
|
+
outdir = Path(tmp)
|
|
90
|
+
args_template = conf.get("igmapper_args", "--profile {url} --outdir {outdir}")
|
|
91
|
+
cmd = [cli] + _formatar_args(args_template, url=profile_url, outdir=str(outdir))
|
|
92
|
+
try:
|
|
93
|
+
resultado = subprocess.run(cmd, capture_output=True, text=True, timeout=900, shell=False)
|
|
94
|
+
except FileNotFoundError as e:
|
|
95
|
+
raise ExtratorNaoConfigurado(f"Não foi possível executar o igmapper: {e}") from e
|
|
96
|
+
if resultado.returncode != 0:
|
|
97
|
+
raise ExtratorErro(f"igmapper terminou com código {resultado.returncode}: {resultado.stderr[-400:]}")
|
|
98
|
+
leads = _parse_resultados_diretorio(outdir, fonte="Instagram")
|
|
99
|
+
if not leads:
|
|
100
|
+
raise ExtratorErro(
|
|
101
|
+
"igmapper rodou mas não retornou dados reconhecíveis. Confira o formato de saída "
|
|
102
|
+
"(esperado CSV/JSON com username, full_name, followers, phone, email)."
|
|
103
|
+
)
|
|
104
|
+
return {"leads": leads, "comando": " ".join(cmd[:4]) + " ..."}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# CNPJ — consulta direta (BrasilAPI) e busca por CNAE/UF (provedor configurável)
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def _so_digitos(texto: str) -> str:
|
|
112
|
+
return "".join(c for c in texto or "" if c.isdigit())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def consulta_cnpj(cnpj: str) -> dict:
|
|
116
|
+
cnpj = _so_digitos(cnpj)
|
|
117
|
+
if len(cnpj) != 14:
|
|
118
|
+
raise ExtratorErro("CNPJ inválido: use 14 dígitos (com ou sem pontuação).")
|
|
119
|
+
try:
|
|
120
|
+
r = httpx.get(f"https://brasilapi.com.br/api/cnpj/v1/{cnpj}", timeout=30)
|
|
121
|
+
except httpx.HTTPError as e:
|
|
122
|
+
raise ExtratorErro(f"Falha ao consultar a BrasilAPI: {e}") from e
|
|
123
|
+
if r.status_code == 404:
|
|
124
|
+
raise ExtratorErro("CNPJ não encontrado na BrasilAPI.")
|
|
125
|
+
if r.status_code >= 400:
|
|
126
|
+
raise ExtratorErro(f"BrasilAPI respondeu HTTP {r.status_code}: {r.text[:200]}")
|
|
127
|
+
d = r.json()
|
|
128
|
+
endereco = " ".join(
|
|
129
|
+
p
|
|
130
|
+
for p in [
|
|
131
|
+
d.get("logradouro"),
|
|
132
|
+
d.get("numero"),
|
|
133
|
+
d.get("bairro"),
|
|
134
|
+
]
|
|
135
|
+
if p
|
|
136
|
+
)
|
|
137
|
+
lead = _novo_lead_parcial()
|
|
138
|
+
lead.update(
|
|
139
|
+
{
|
|
140
|
+
"nome": d.get("razao_social") or d.get("nome_fantasia") or "",
|
|
141
|
+
"empresa": d.get("nome_fantasia") or d.get("razao_social") or "",
|
|
142
|
+
"cnpj": cnpj,
|
|
143
|
+
"telefone": d.get("ddd_telefone_1") or "",
|
|
144
|
+
"endereco": endereco,
|
|
145
|
+
"cidade": f"{d.get('municipio', '')}/{d.get('uf', '')}",
|
|
146
|
+
"email": "",
|
|
147
|
+
"fonte": "CNPJ",
|
|
148
|
+
"etapa": "leads",
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
lead["score"] = 55
|
|
152
|
+
return lead
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def buscar_cnpj_cnae(cnae: str, uf: str, quantidade: int = 20) -> dict:
|
|
156
|
+
"""Busca empresas por CNAE + UF no provedor configurável (padrão: OpenCNPJ).
|
|
157
|
+
|
|
158
|
+
O formato pode variar; o parser é tolerante (aceita lista, {"results": ...},
|
|
159
|
+
{"data": ...} e vários nomes de campos).
|
|
160
|
+
"""
|
|
161
|
+
conf = _conf()
|
|
162
|
+
base = (conf.get("cnpj_provedor_busca") or "https://api.opencnpj.org").rstrip("/")
|
|
163
|
+
params = {"cnae": _so_digitos(cnae), "uf": uf.strip().upper(), "quantidade": str(max(1, min(quantidade, 100)))}
|
|
164
|
+
try:
|
|
165
|
+
r = httpx.get(base, params=params, timeout=60)
|
|
166
|
+
except httpx.HTTPError as e:
|
|
167
|
+
raise ExtratorErro(
|
|
168
|
+
f"Falha ao consultar o provedor de busca ({base}): {e}. "
|
|
169
|
+
"Verifique a URL do provedor em Ferramentas → Extratores."
|
|
170
|
+
) from e
|
|
171
|
+
if r.status_code >= 400:
|
|
172
|
+
raise ExtratorErro(
|
|
173
|
+
f"Provedor de busca ({base}) respondeu HTTP {r.status_code}: {r.text[:200]}. "
|
|
174
|
+
"O provedor é configurável em Ferramentas → Extratores."
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
corpo = r.json()
|
|
178
|
+
except ValueError:
|
|
179
|
+
raise ExtratorErro(f"Provedor de busca devolveu resposta não-JSON: {r.text[:200]}")
|
|
180
|
+
itens = []
|
|
181
|
+
if isinstance(corpo, list):
|
|
182
|
+
itens = corpo
|
|
183
|
+
elif isinstance(corpo, dict):
|
|
184
|
+
itens = corpo.get("results") or corpo.get("data") or corpo.get("items") or []
|
|
185
|
+
leads = []
|
|
186
|
+
for item in itens[:quantidade]:
|
|
187
|
+
if not isinstance(item, dict):
|
|
188
|
+
continue
|
|
189
|
+
lead = _novo_lead_parcial()
|
|
190
|
+
lead.update(
|
|
191
|
+
{
|
|
192
|
+
"nome": _primeiro(item, "razao_social", "nome", "razaoSocial", "company_name"),
|
|
193
|
+
"empresa": _primeiro(item, "nome_fantasia", "fantasia", "nomeFantasia", "trade_name") or "",
|
|
194
|
+
"cnpj": _so_digitos(_primeiro(item, "cnpj", "cnpj_raiz", "document")),
|
|
195
|
+
"telefone": _primeiro(item, "telefone", "phone", "ddd_telefone_1") or "",
|
|
196
|
+
"email": _primeiro(item, "email", "contact_email") or "",
|
|
197
|
+
"cidade": f"{_primeiro(item, 'municipio', 'city', 'municipio') or ''}/{_primeiro(item, 'uf', 'state', 'uf') or ''}",
|
|
198
|
+
"fonte": "CNPJ",
|
|
199
|
+
"etapa": "leads",
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
lead["score"] = 45
|
|
203
|
+
leads.append(lead)
|
|
204
|
+
return {"leads": leads, "provedor": base}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def enriquecer_com_site(website: str) -> dict:
|
|
208
|
+
"""Scraping profundo de contatos no site da empresa, via Scrapling (opcional)."""
|
|
209
|
+
conf = _conf()
|
|
210
|
+
cli = conf.get("scrapling_cli", "")
|
|
211
|
+
if not cli or not Path(cli).exists():
|
|
212
|
+
raise ExtratorNaoConfigurado(
|
|
213
|
+
"Scrapling não configurado. Instale https://github.com/D4Vinci/Scrapling "
|
|
214
|
+
"e informe o caminho em Ferramentas → Extratores."
|
|
215
|
+
)
|
|
216
|
+
try:
|
|
217
|
+
resultado = subprocess.run(
|
|
218
|
+
[cli, "extract", website],
|
|
219
|
+
capture_output=True,
|
|
220
|
+
text=True,
|
|
221
|
+
timeout=300,
|
|
222
|
+
shell=False,
|
|
223
|
+
)
|
|
224
|
+
except FileNotFoundError as e:
|
|
225
|
+
raise ExtratorNaoConfigurado(f"Não foi possível executar o Scrapling: {e}") from e
|
|
226
|
+
if resultado.returncode != 0:
|
|
227
|
+
raise ExtratorErro(f"Scrapling falhou: {resultado.stderr[-400:]}")
|
|
228
|
+
return {"saidas": resultado.stdout[-2000:]}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
# Utilitários de parsing tolerante
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def _primeiro(d: dict, *chaves: str):
|
|
236
|
+
for chave in chaves:
|
|
237
|
+
if d.get(chave):
|
|
238
|
+
return d[chave]
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _formatar_args(template: str, **valores: str) -> list[str]:
|
|
243
|
+
"""Converte '--kw "a b" --n 5' em ['--kw', 'a b', '--n', '5'] respeitando aspas."""
|
|
244
|
+
texto = template.format(**valores)
|
|
245
|
+
partes, atual, aspas = [], "", None
|
|
246
|
+
for ch in texto:
|
|
247
|
+
if ch in "\"'":
|
|
248
|
+
aspas = None if aspas == ch else ch
|
|
249
|
+
continue
|
|
250
|
+
if ch == " " and not aspas:
|
|
251
|
+
if atual:
|
|
252
|
+
partes.append(atual)
|
|
253
|
+
atual = ""
|
|
254
|
+
else:
|
|
255
|
+
atual += ch
|
|
256
|
+
if atual:
|
|
257
|
+
partes.append(atual)
|
|
258
|
+
return partes
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _mapear_linha(linha: dict, fonte: str) -> dict:
|
|
262
|
+
lead = _novo_lead_parcial()
|
|
263
|
+
nome = _primeiro(linha, "nome", "name", "title", "razao_social", "full_name", "username")
|
|
264
|
+
lead.update(
|
|
265
|
+
{
|
|
266
|
+
"nome": nome or "Sem nome",
|
|
267
|
+
"telefone": _primeiro(linha, "telefone", "phone", "phone_number", "public_phone", "ddd_telefone_1"),
|
|
268
|
+
"email": _primeiro(linha, "email", "email_address", "business_email"),
|
|
269
|
+
"empresa": _primeiro(linha, "empresa", "company", "site_name", "nome_fantasia"),
|
|
270
|
+
"endereco": _primeiro(linha, "endereco", "address", "full_address", "street"),
|
|
271
|
+
"cidade": _primeiro(linha, "cidade", "city", "locality"),
|
|
272
|
+
"site": _primeiro(linha, "site", "website", "web_site"),
|
|
273
|
+
"fonte": fonte,
|
|
274
|
+
"etapa": "leads",
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
extras = {}
|
|
278
|
+
if _primeiro(linha, "rating", "nota"):
|
|
279
|
+
extras["avaliacao"] = linha.get("rating") or linha.get("nota")
|
|
280
|
+
if _primeiro(linha, "reviews", "avaliacoes"):
|
|
281
|
+
extras["qtd_avaliacoes"] = linha.get("reviews") or linha.get("avaliacoes")
|
|
282
|
+
if _primeiro(linha, "followers", "seguidores"):
|
|
283
|
+
extras["seguidores"] = linha.get("followers") or linha.get("seguidores")
|
|
284
|
+
if _primeiro(linha, "biography", "bio"):
|
|
285
|
+
extras["bio"] = (linha.get("biography") or linha.get("bio"))[:200]
|
|
286
|
+
lead["extras"] = extras
|
|
287
|
+
lead["score"] = 40 + (10 if lead["telefone"] else 0) + (10 if lead["email"] else 0)
|
|
288
|
+
return lead
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _parse_resultados_diretorio(outdir: Path, fonte: str) -> list:
|
|
292
|
+
leads = []
|
|
293
|
+
for arquivo in sorted(outdir.iterdir()):
|
|
294
|
+
if arquivo.suffix.lower() == ".csv":
|
|
295
|
+
with arquivo.open(encoding="utf-8-sig", newline="") as fh:
|
|
296
|
+
for linha in csv.DictReader(fh):
|
|
297
|
+
lead = _mapear_linha(linha, fonte)
|
|
298
|
+
if lead["nome"] != "Sem nome":
|
|
299
|
+
leads.append(lead)
|
|
300
|
+
elif arquivo.suffix.lower() == ".json":
|
|
301
|
+
try:
|
|
302
|
+
dados = json.loads(arquivo.read_text(encoding="utf-8"))
|
|
303
|
+
except (ValueError, OSError):
|
|
304
|
+
continue
|
|
305
|
+
itens = dados if isinstance(dados, list) else [dados]
|
|
306
|
+
for item in itens:
|
|
307
|
+
if isinstance(item, dict):
|
|
308
|
+
lead = _mapear_linha(item, fonte)
|
|
309
|
+
if lead["nome"] != "Sem nome":
|
|
310
|
+
leads.append(lead)
|
|
311
|
+
# dedup por telefone/email
|
|
312
|
+
vistos, unicos = set(), []
|
|
313
|
+
for lead in leads:
|
|
314
|
+
chave = (lead.get("telefone") or "", (lead.get("email") or "").lower())
|
|
315
|
+
if chave[0] and chave[0] in vistos:
|
|
316
|
+
continue
|
|
317
|
+
if chave[1] and chave[1] in vistos:
|
|
318
|
+
continue
|
|
319
|
+
vistos.add(chave[0])
|
|
320
|
+
vistos.add(chave[1])
|
|
321
|
+
unicos.append(lead)
|
|
322
|
+
return unicos
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Cliente LLM (API OpenAI-compatível) para os agentes do LeadMaster.
|
|
2
|
+
|
|
3
|
+
A configuração fica em `config.json → llm` (provedor, base_url, api_key, modelo).
|
|
4
|
+
Usado pelo agente de CRM (enriquecimento/sugestões) e pelo agente de atendimento.
|
|
5
|
+
Se o LLM não estiver configurado, as heurísticas locais assumem e a mensagem
|
|
6
|
+
sempre informa o que fazer — nunca fingimos uma resposta de IA.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from . import storage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMNaoConfigurado(Exception):
|
|
15
|
+
"""LLM não configurado — mensagem acionável para Ferramentas → LLM."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LLMErro(Exception):
|
|
19
|
+
"""LLM configurado, mas a chamada falhou."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _conf() -> dict:
|
|
23
|
+
return storage.get_config().get("llm", {})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def configurado() -> bool:
|
|
27
|
+
c = _conf()
|
|
28
|
+
return bool(c.get("api_key") and c.get("base_url"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def chat(mensagens: list, temperature: float = 0.7, max_tokens: int = 2000) -> str:
|
|
32
|
+
c = _conf()
|
|
33
|
+
if not configurado():
|
|
34
|
+
raise LLMNaoConfigurado(
|
|
35
|
+
"LLM não configurado. Vá em Ferramentas → LLM, informe base_url, api_key e modelo."
|
|
36
|
+
)
|
|
37
|
+
url = c["base_url"].rstrip("/") + "/chat/completions"
|
|
38
|
+
try:
|
|
39
|
+
r = httpx.post(
|
|
40
|
+
url,
|
|
41
|
+
headers={"Authorization": f"Bearer {c['api_key']}"},
|
|
42
|
+
json={
|
|
43
|
+
"model": c.get("modelo", "gpt-4o-mini"),
|
|
44
|
+
"messages": mensagens,
|
|
45
|
+
"temperature": temperature,
|
|
46
|
+
"max_tokens": max_tokens,
|
|
47
|
+
},
|
|
48
|
+
timeout=180,
|
|
49
|
+
)
|
|
50
|
+
r.raise_for_status()
|
|
51
|
+
return r.json()["choices"][0]["message"]["content"]
|
|
52
|
+
except httpx.HTTPStatusError as e:
|
|
53
|
+
raise LLMErro(f"LLM respondeu HTTP {e.response.status_code}: {e.response.text[:300]}") from e
|
|
54
|
+
except httpx.HTTPError as e:
|
|
55
|
+
raise LLMErro(f"Falha de conexão com o LLM ({url}): {e}") from e
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def testar_conexao() -> dict:
|
|
59
|
+
try:
|
|
60
|
+
resposta = chat(
|
|
61
|
+
[{"role": "user", "content": "Responda apenas com: OK"}],
|
|
62
|
+
temperature=0,
|
|
63
|
+
max_tokens=8,
|
|
64
|
+
)
|
|
65
|
+
return {"ok": True, "resposta": resposta.strip()[:100]}
|
|
66
|
+
except LLMNaoConfigurado as e:
|
|
67
|
+
return {"ok": False, "mensagem": str(e)}
|
|
68
|
+
except LLMErro as e:
|
|
69
|
+
return {"ok": False, "mensagem": str(e)}
|