@danhachuel/leadmaster 0.0.0-stage → 0.1.0
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 +59 -0
- package/LICENSE +21 -0
- package/README.md +128 -2
- package/backend/main.py +523 -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 +189 -0
- package/backend/services/moneyprinter.py +102 -0
- package/backend/services/openmontage.py +62 -0
- package/backend/services/pipeline.py +515 -0
- package/backend/services/short_video_maker.py +72 -0
- package/backend/services/storage.py +460 -0
- package/backend/services/youtube_upload.py +163 -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-CANBU7DH.js +93 -0
- package/frontend/dist/assets/index-eswbuT3S.css +5 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/index.html +14 -0
- package/package.json +49 -4
- package/scripts/cli.mjs +512 -0
package/backend/main.py
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""LeadMaster — API FastAPI (100% local).
|
|
2
|
+
|
|
3
|
+
Rotas da aplicação + serving estático da UI construída (frontend/dist) para o
|
|
4
|
+
modo `npx`, onde um único servidor entrega painel e API na mesma porta.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from fastapi import FastAPI, HTTPException
|
|
16
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
17
|
+
from fastapi.responses import FileResponse
|
|
18
|
+
from fastapi.staticfiles import StaticFiles
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
from services import agents, crm_agent, extractors, llm, moneyprinter, pipeline, short_video_maker, storage, youtube_upload
|
|
22
|
+
|
|
23
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
24
|
+
FRONTEND_DIST = REPO_ROOT / "frontend" / "dist"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _versao() -> str:
|
|
28
|
+
env = os.environ.get("LEADMASTER_VERSION")
|
|
29
|
+
if env:
|
|
30
|
+
return env
|
|
31
|
+
try:
|
|
32
|
+
return json.loads((REPO_ROOT / "package.json").read_text(encoding="utf-8"))["version"]
|
|
33
|
+
except Exception:
|
|
34
|
+
return "dev"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
VERSION = _versao()
|
|
38
|
+
engine = pipeline.PipelineEngine()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@asynccontextmanager
|
|
42
|
+
async def lifespan(_app: FastAPI):
|
|
43
|
+
storage.ensure_seeds()
|
|
44
|
+
engine.recuperar()
|
|
45
|
+
engine.iniciar_agendador()
|
|
46
|
+
yield
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
app = FastAPI(title="LeadMaster API", version=VERSION, lifespan=lifespan)
|
|
50
|
+
|
|
51
|
+
app.add_middleware(
|
|
52
|
+
CORSMiddleware,
|
|
53
|
+
# A UI local (npx) roda same-origin; em modo dev o Vite sobe em 5055 e pode
|
|
54
|
+
# auto-incrementar (5056, 5057...) quando a porta está ocupada — por isso
|
|
55
|
+
# aceitamos qualquer porta em localhost/127.0.0.1.
|
|
56
|
+
allow_origin_regex=r"https?://(localhost|127\.0\.0\.1)(:\d+)?",
|
|
57
|
+
allow_methods=["*"],
|
|
58
|
+
allow_headers=["*"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Models
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class LeadIn(BaseModel):
|
|
67
|
+
nome: str = Field(min_length=1)
|
|
68
|
+
telefone: str = ""
|
|
69
|
+
email: str = ""
|
|
70
|
+
fonte: str = "Manual"
|
|
71
|
+
etapa: str = "leads"
|
|
72
|
+
cnpj: str = ""
|
|
73
|
+
empresa: str = ""
|
|
74
|
+
endereco: str = ""
|
|
75
|
+
cidade: str = ""
|
|
76
|
+
observacoes: str = ""
|
|
77
|
+
score: int | None = None
|
|
78
|
+
tags: list[str] = []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class LeadsImportIn(BaseModel):
|
|
82
|
+
leads: list[dict]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class CampaignIn(BaseModel):
|
|
86
|
+
nome: str = Field(min_length=1)
|
|
87
|
+
canal: str = "texto"
|
|
88
|
+
template: str = "texto"
|
|
89
|
+
lead_ids: list[str] = []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class JobIn(BaseModel):
|
|
93
|
+
tema: str = Field(min_length=1)
|
|
94
|
+
titulo: str = ""
|
|
95
|
+
tom: str = "informativo"
|
|
96
|
+
duracao_min: int = 8
|
|
97
|
+
keyword_seo: str = ""
|
|
98
|
+
formatos: list[str] = ["wide", "shorts"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AcaoIn(BaseModel):
|
|
102
|
+
acao: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class GoogleMapsIn(BaseModel):
|
|
106
|
+
palavra_chave: str = Field(min_length=2)
|
|
107
|
+
localizacao: str = Field(min_length=2)
|
|
108
|
+
max_resultados: int = 100
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class InstagramIn(BaseModel):
|
|
112
|
+
profile_url: str = Field(min_length=6)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CnpjIn(BaseModel):
|
|
116
|
+
cnpj: str = Field(min_length=8)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CnpjSearchIn(BaseModel):
|
|
120
|
+
cnae: str = Field(min_length=2)
|
|
121
|
+
uf: str = Field(min_length=2)
|
|
122
|
+
quantidade: int = 20
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ChatIn(BaseModel):
|
|
126
|
+
mensagens: list[dict]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TemplateTestIn(BaseModel):
|
|
130
|
+
template: str
|
|
131
|
+
lead_id: str | None = None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WebhookTestIn(BaseModel):
|
|
135
|
+
url: str = Field(min_length=8)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class IntegracaoTestIn(BaseModel):
|
|
139
|
+
url: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Sistema
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
@app.get("/api/health")
|
|
147
|
+
def health():
|
|
148
|
+
return {
|
|
149
|
+
"status": "ok",
|
|
150
|
+
"versao": VERSION,
|
|
151
|
+
"home": str(storage.get_home()),
|
|
152
|
+
"storage": str(storage.storage_dir()),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.get("/api/doctor")
|
|
157
|
+
def doctor():
|
|
158
|
+
integracoes = {
|
|
159
|
+
"moneyprinter": moneyprinter.verificar(),
|
|
160
|
+
"short_video_maker": short_video_maker.verificar(),
|
|
161
|
+
"uploader_youtube": youtube_upload.verificar(),
|
|
162
|
+
"llm_configurado": llm.configurado(),
|
|
163
|
+
"openmontage": bool(storage.get_config()["integracoes"]["openmontage"].get("cli_path")),
|
|
164
|
+
"openoutreach": agents.disparo_configurado(),
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
"ok": True,
|
|
168
|
+
"versao": VERSION,
|
|
169
|
+
"python": platform.python_version(),
|
|
170
|
+
"sistema": platform.platform(),
|
|
171
|
+
"dist_frontend": FRONTEND_DIST.exists(),
|
|
172
|
+
"storage": str(storage.storage_dir()),
|
|
173
|
+
"uploads_hoje": youtube_upload.contagem_hoje(),
|
|
174
|
+
"integracoes": integracoes,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.post("/api/shutdown")
|
|
179
|
+
def shutdown():
|
|
180
|
+
threading.Timer(0.6, os._exit, [0]).start()
|
|
181
|
+
return {"ok": True, "mensagem": "Servidor local encerrando em instantes."}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Métricas (Painel)
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
@app.get("/api/metrics")
|
|
189
|
+
def metrics():
|
|
190
|
+
leads = storage.list_leads()
|
|
191
|
+
campaigns = storage.list_campaigns()
|
|
192
|
+
enviadas = sum(int(c.get("enviadas", 0)) for c in campaigns)
|
|
193
|
+
erros = sum(int(c.get("erros", 0)) for c in campaigns)
|
|
194
|
+
total_envios = enviadas + erros
|
|
195
|
+
jobs = storage.list_jobs()
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"conexao": {
|
|
199
|
+
"geral": True, # o backend local responde => conectado
|
|
200
|
+
"moneyprinter": moneyprinter.verificar(),
|
|
201
|
+
"short_video_maker": short_video_maker.verificar(),
|
|
202
|
+
"uploader_youtube": youtube_upload.verificar(),
|
|
203
|
+
"llm": llm.configurado(),
|
|
204
|
+
"openoutreach": agents.disparo_configurado(),
|
|
205
|
+
},
|
|
206
|
+
"taxa_entrega": round(enviadas / total_envios * 100, 1) if total_envios else 100.0,
|
|
207
|
+
"taxa_erro": round(erros / total_envios * 100, 1) if total_envios else 0.0,
|
|
208
|
+
"total_enviadas": enviadas,
|
|
209
|
+
"total_erros": erros,
|
|
210
|
+
"leads_total": len(leads),
|
|
211
|
+
"leads_por_etapa": {
|
|
212
|
+
etapa: sum(1 for l in leads if l.get("etapa") == etapa)
|
|
213
|
+
for etapa in ("leads", "em_contato", "proposta", "fechado")
|
|
214
|
+
},
|
|
215
|
+
"campanhas": {"enviadas": enviadas, "erros": erros, "total": len(campaigns)},
|
|
216
|
+
"pipeline": {
|
|
217
|
+
"ativos": sum(1 for j in jobs if j.get("status") in ("em_andamento", "pausado")),
|
|
218
|
+
"concluidos": sum(1 for j in jobs if j.get("status") == "concluido"),
|
|
219
|
+
},
|
|
220
|
+
"perfil": storage.get_config().get("perfil", {}),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
# Leads
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def _validar_etapa(etapa: str) -> str:
|
|
229
|
+
if etapa not in ("leads", "em_contato", "proposta", "fechado"):
|
|
230
|
+
raise HTTPException(400, "Etapa inválida. Use: leads, em_contato, proposta ou fechado.")
|
|
231
|
+
return etapa
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@app.get("/api/leads")
|
|
235
|
+
def listar_leads():
|
|
236
|
+
return storage.list_leads()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.post("/api/leads", status_code=201)
|
|
240
|
+
def criar_lead(lead_in: LeadIn):
|
|
241
|
+
lead = lead_in.model_dump()
|
|
242
|
+
lead["id"] = storage._novo_id()
|
|
243
|
+
lead["etapa"] = _validar_etapa(lead.get("etapa", "leads"))
|
|
244
|
+
lead["score"] = lead.get("score") if lead.get("score") is not None else crm_agent.calcular_completude(lead)
|
|
245
|
+
storage.add_lead(lead)
|
|
246
|
+
agents.disparar_webhook("lead.criado", {"lead_id": lead["id"], "nome": lead["nome"]})
|
|
247
|
+
return lead
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.patch("/api/leads/{lead_id}")
|
|
251
|
+
def atualizar_lead(lead_id: str, patch: dict):
|
|
252
|
+
if patch.get("etapa"):
|
|
253
|
+
_validar_etapa(patch["etapa"])
|
|
254
|
+
atualizado = storage.update_lead(lead_id, patch)
|
|
255
|
+
if not atualizado:
|
|
256
|
+
raise HTTPException(404, "Lead não encontrado.")
|
|
257
|
+
if patch.get("etapa"):
|
|
258
|
+
agents.disparar_webhook("lead.etapa", {"lead_id": lead_id, "etapa": patch["etapa"]})
|
|
259
|
+
return atualizado
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@app.delete("/api/leads/{lead_id}")
|
|
263
|
+
def remover_lead(lead_id: str):
|
|
264
|
+
if not storage.delete_lead(lead_id):
|
|
265
|
+
raise HTTPException(404, "Lead não encontrado.")
|
|
266
|
+
return {"ok": True}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@app.post("/api/leads/import")
|
|
270
|
+
def importar_leads(dados: LeadsImportIn):
|
|
271
|
+
for lead in dados.leads:
|
|
272
|
+
lead["etapa"] = _validar_etapa(lead.get("etapa", "leads"))
|
|
273
|
+
return storage.import_leads(dados.leads)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@app.post("/api/leads/{lead_id}/enrich")
|
|
277
|
+
def enriquecer_lead(lead_id: str):
|
|
278
|
+
resultado = crm_agent.enriquecer_lead(lead_id)
|
|
279
|
+
if not resultado.get("ok"):
|
|
280
|
+
raise HTTPException(404, resultado.get("mensagem", "Lead não encontrado."))
|
|
281
|
+
return resultado
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# CRM (agente)
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
@app.get("/api/crm/sugestoes")
|
|
289
|
+
def sugestoes_crm():
|
|
290
|
+
return crm_agent.sugerir_movimentacoes()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.post("/api/crm/sugestoes/aplicar")
|
|
294
|
+
def aplicar_sugestao(dados: dict):
|
|
295
|
+
lead_id, para = dados.get("lead_id"), dados.get("para")
|
|
296
|
+
if not lead_id or not para:
|
|
297
|
+
raise HTTPException(400, "Informe lead_id e para.")
|
|
298
|
+
atualizado = storage.update_lead(lead_id, {"etapa": _validar_etapa(para)})
|
|
299
|
+
if not atualizado:
|
|
300
|
+
raise HTTPException(404, "Lead não encontrado.")
|
|
301
|
+
agents.disparar_webhook("lead.etapa", {"lead_id": lead_id, "etapa": para, "via": "agente_crm"})
|
|
302
|
+
return {"ok": True, "lead": atualizado}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Campanhas / Disparos
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
@app.get("/api/campaigns")
|
|
310
|
+
def listar_campanhas():
|
|
311
|
+
return storage.list_campaigns()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.post("/api/campaigns", status_code=201)
|
|
315
|
+
def criar_campanha(campaign_in: CampaignIn):
|
|
316
|
+
campaign = campaign_in.model_dump()
|
|
317
|
+
campaign["id"] = storage._novo_id()
|
|
318
|
+
campaign.update({"status": "rascunho", "total": len(campaign["lead_ids"]), "enviadas": 0, "erros": 0, "eventos": [], "criado_em": storage.now_iso()})
|
|
319
|
+
storage.upsert_campaign(campaign)
|
|
320
|
+
return campaign
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@app.post("/api/campaigns/{campaign_id}/start")
|
|
324
|
+
def iniciar_campanha(campaign_id: str):
|
|
325
|
+
resultado = agents.iniciar_campanha(campaign_id)
|
|
326
|
+
if not resultado.get("ok"):
|
|
327
|
+
raise HTTPException(400, resultado["mensagem"])
|
|
328
|
+
return resultado
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.post("/api/campaigns/{campaign_id}/pause")
|
|
332
|
+
def pausar_campanha(campaign_id: str):
|
|
333
|
+
return agents.pausar_campanha(campaign_id)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@app.post("/api/campaigns/{campaign_id}/stop")
|
|
337
|
+
def parar_campanha(campaign_id: str):
|
|
338
|
+
return agents.parar_campanha(campaign_id)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@app.delete("/api/campaigns/{campaign_id}")
|
|
342
|
+
def remover_campanha(campaign_id: str):
|
|
343
|
+
if not storage.delete_campaign(campaign_id):
|
|
344
|
+
raise HTTPException(404, "Campanha não encontrada.")
|
|
345
|
+
return {"ok": True}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
# ---------------------------------------------------------------------------
|
|
349
|
+
# Pipeline de vídeos
|
|
350
|
+
# ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
@app.get("/api/pipeline")
|
|
353
|
+
def listar_pipeline():
|
|
354
|
+
return engine.listar()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@app.post("/api/pipeline", status_code=201)
|
|
358
|
+
def criar_job(job_in: JobIn):
|
|
359
|
+
try:
|
|
360
|
+
return engine.criar_job(job_in.model_dump())
|
|
361
|
+
except ValueError as e:
|
|
362
|
+
raise HTTPException(400, str(e))
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@app.post("/api/pipeline/{job_id}/actions")
|
|
366
|
+
def agir_pipeline(job_id: str, acao_in: AcaoIn):
|
|
367
|
+
try:
|
|
368
|
+
resultado = engine.agir(job_id, acao_in.acao)
|
|
369
|
+
except ValueError as e:
|
|
370
|
+
raise HTTPException(400, str(e))
|
|
371
|
+
if not resultado.get("ok"):
|
|
372
|
+
raise HTTPException(400, resultado.get("mensagem", "Ação não permitida."))
|
|
373
|
+
return resultado
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Extratores
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
@app.post("/api/extract/google-maps")
|
|
381
|
+
def extrair_google_maps(dados: GoogleMapsIn):
|
|
382
|
+
try:
|
|
383
|
+
return extractors.extrair_google_maps(dados.palavra_chave, dados.localizacao, dados.max_resultados)
|
|
384
|
+
except extractors.ExtratorNaoConfigurado as e:
|
|
385
|
+
raise HTTPException(400, str(e))
|
|
386
|
+
except extractors.ExtratorErro as e:
|
|
387
|
+
raise HTTPException(502, str(e))
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@app.post("/api/extract/instagram")
|
|
391
|
+
def extrair_instagram(dados: InstagramIn):
|
|
392
|
+
try:
|
|
393
|
+
return extractors.extrair_instagram(dados.profile_url)
|
|
394
|
+
except extractors.ExtratorNaoConfigurado as e:
|
|
395
|
+
raise HTTPException(400, str(e))
|
|
396
|
+
except extractors.ExtratorErro as e:
|
|
397
|
+
raise HTTPException(502, str(e))
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@app.post("/api/extract/cnpj")
|
|
401
|
+
def consultar_cnpj(dados: CnpjIn):
|
|
402
|
+
try:
|
|
403
|
+
return extractors.consulta_cnpj(dados.cnpj)
|
|
404
|
+
except extractors.ExtratorErro as e:
|
|
405
|
+
raise HTTPException(502, str(e))
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@app.post("/api/extract/cnpj/search")
|
|
409
|
+
def buscar_cnpj(dados: CnpjSearchIn):
|
|
410
|
+
try:
|
|
411
|
+
return extractors.buscar_cnpj_cnae(dados.cnae, dados.uf, dados.quantidade)
|
|
412
|
+
except extractors.ExtratorErro as e:
|
|
413
|
+
raise HTTPException(502, str(e))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# Configuração e integrações
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
@app.get("/api/config")
|
|
421
|
+
def obter_config():
|
|
422
|
+
return storage.redact_config(storage.get_config())
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
@app.put("/api/config")
|
|
426
|
+
def salvar_config(patch: dict):
|
|
427
|
+
storage.update_config(patch)
|
|
428
|
+
return storage.redact_config(storage.get_config())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@app.post("/api/config/test/llm")
|
|
432
|
+
def testar_llm():
|
|
433
|
+
return llm.testar_conexao()
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@app.post("/api/config/test/integracao")
|
|
437
|
+
def testar_integracao(dados: IntegracaoTestIn):
|
|
438
|
+
import httpx
|
|
439
|
+
|
|
440
|
+
try:
|
|
441
|
+
r = httpx.get(dados.url, timeout=4)
|
|
442
|
+
return {"ok": True, "status_http": r.status_code}
|
|
443
|
+
except httpx.HTTPError as e:
|
|
444
|
+
return {"ok": False, "mensagem": f"Sem resposta de {dados.url}: {e}"}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@app.get("/api/integrations/status")
|
|
448
|
+
def status_integracoes():
|
|
449
|
+
return {
|
|
450
|
+
"moneyprinter": moneyprinter.verificar(),
|
|
451
|
+
"short_video_maker": short_video_maker.verificar(),
|
|
452
|
+
"uploader_youtube": youtube_upload.verificar(),
|
|
453
|
+
"llm": llm.configurado(),
|
|
454
|
+
"openmontage": bool(storage.get_config()["integracoes"]["openmontage"].get("cli_path")),
|
|
455
|
+
"openoutreach": agents.disparo_configurado(),
|
|
456
|
+
"pipecat": bool(storage.get_config()["integracoes"]["pipecat"].get("url")),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
# Agentes
|
|
462
|
+
# ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
@app.get("/api/agents/templates")
|
|
465
|
+
def obter_templates():
|
|
466
|
+
return agents.obter_templates()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.put("/api/agents/templates")
|
|
470
|
+
def salvar_templates(patch: dict):
|
|
471
|
+
return agents.salvar_templates(patch)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@app.post("/api/agents/templates/test")
|
|
475
|
+
def testar_template(dados: TemplateTestIn):
|
|
476
|
+
return agents.testar_template(dados.template, dados.lead_id)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@app.post("/api/agents/attendance/chat")
|
|
480
|
+
def atendimento_chat(dados: ChatIn):
|
|
481
|
+
return agents.atendimento_chat(dados.mensagens)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ---------------------------------------------------------------------------
|
|
485
|
+
# Webhook
|
|
486
|
+
# ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
@app.get("/api/webhook")
|
|
489
|
+
def obter_webhook():
|
|
490
|
+
return storage.get_config().get("webhook", {})
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@app.put("/api/webhook")
|
|
494
|
+
def salvar_webhook(patch: dict):
|
|
495
|
+
config = storage.get_config()
|
|
496
|
+
webhook = config.get("webhook", {})
|
|
497
|
+
webhook.update({k: v for k, v in patch.items() if k in ("url", "eventos")})
|
|
498
|
+
storage.update_config({"webhook": webhook})
|
|
499
|
+
return webhook
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@app.post("/api/webhook/test")
|
|
503
|
+
def testar_webhook(dados: WebhookTestIn):
|
|
504
|
+
return agents.testar_webhook(dados.url)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ---------------------------------------------------------------------------
|
|
508
|
+
# UI estática (modo npx / produção)
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
if FRONTEND_DIST.exists():
|
|
512
|
+
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
|
|
513
|
+
|
|
514
|
+
@app.get("/", include_in_schema=False)
|
|
515
|
+
def index():
|
|
516
|
+
return FileResponse(FRONTEND_DIST / "index.html")
|
|
517
|
+
|
|
518
|
+
@app.get("/{full_path:path}", include_in_schema=False)
|
|
519
|
+
def spa(full_path: str):
|
|
520
|
+
alvo = FRONTEND_DIST / full_path
|
|
521
|
+
if full_path and alvo.is_file():
|
|
522
|
+
return FileResponse(alvo)
|
|
523
|
+
return FileResponse(FRONTEND_DIST / "index.html")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Serviços e regras de negócio do LeadMaster.
|