Spaces:
Runtime error
Runtime error
Upload 3 files
Browse files- api.py +51 -25
- app.py +19 -31
- requirements.txt +1 -1
api.py
CHANGED
|
@@ -17,7 +17,6 @@ try:
|
|
| 17 |
os.environ.setdefault("GRPC_DEFAULT_SSL_ROOTS_FILE_PATH", CERTIFI_CA_BUNDLE)
|
| 18 |
except Exception:
|
| 19 |
pass
|
| 20 |
-
import google.generativeai as genai
|
| 21 |
import json
|
| 22 |
import imaplib
|
| 23 |
import email
|
|
@@ -35,11 +34,40 @@ from logging.handlers import RotatingFileHandler
|
|
| 35 |
from bs4 import BeautifulSoup
|
| 36 |
import sys
|
| 37 |
import asyncio
|
| 38 |
-
import crypto
|
| 39 |
|
| 40 |
if sys.platform == "win32":
|
| 41 |
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
| 42 |
-
load_dotenv()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# --- 1. LOGGING CON ROTACIÓN (max 2MB, 3 backups) ---
|
| 45 |
log_handler = RotatingFileHandler('backend.log', maxBytes=2*1024*1024, backupCount=3, encoding='utf-8')
|
|
@@ -294,8 +322,8 @@ async def analizar_pliego(
|
|
| 294 |
started_at = time.perf_counter()
|
| 295 |
api_key_clean = gemini_key.strip()
|
| 296 |
try:
|
| 297 |
-
|
| 298 |
-
archivos_subidos = []
|
| 299 |
|
| 300 |
for archivo in archivos_pdf:
|
| 301 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
|
|
@@ -303,19 +331,23 @@ async def analizar_pliego(
|
|
| 303 |
tmp.write(content)
|
| 304 |
tmp_path = tmp.name
|
| 305 |
|
| 306 |
-
uploaded_file =
|
| 307 |
archivos_subidos.append(uploaded_file)
|
| 308 |
os.remove(tmp_path)
|
| 309 |
|
| 310 |
# gemini-2.5-flash para análisis complejo de PDFs
|
| 311 |
-
|
| 312 |
-
response =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
try:
|
| 314 |
db.log_ai_usage(
|
| 315 |
username=username,
|
| 316 |
role=role,
|
| 317 |
action="analizar_pliego",
|
| 318 |
-
model=
|
| 319 |
usage_metadata=response.usage_metadata,
|
| 320 |
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
| 321 |
metadata={"pdf_count": len(archivos_subidos)}
|
|
@@ -324,12 +356,9 @@ async def analizar_pliego(
|
|
| 324 |
logger.warning(f"Error logging metric: {e}")
|
| 325 |
|
| 326 |
|
| 327 |
-
# Limpieza de archivos en la nube de Gemini para evitar llenar la cuota
|
| 328 |
-
for f in archivos_subidos:
|
| 329 |
-
|
| 330 |
-
genai.delete_file(f.name)
|
| 331 |
-
except Exception as e:
|
| 332 |
-
logger.warning(f"No se pudo borrar archivo temporal de Gemini: {e}")
|
| 333 |
|
| 334 |
logger.info(f"{len(archivos_subidos)} pliego(s) analizados exitosamente.")
|
| 335 |
data = json.loads(response.text)
|
|
@@ -343,7 +372,7 @@ async def analizar_pliego(
|
|
| 343 |
module="ai",
|
| 344 |
action="analizar_pliego",
|
| 345 |
provider="gemini",
|
| 346 |
-
model=
|
| 347 |
status="error",
|
| 348 |
error_message=str(e)[:500],
|
| 349 |
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
@@ -382,9 +411,8 @@ def procesar_correos_background(username: str, servidor_imap: str, licitacion_ac
|
|
| 382 |
return
|
| 383 |
|
| 384 |
try:
|
| 385 |
-
|
| 386 |
# gemini-2.5-flash para clasificación simple de correos
|
| 387 |
-
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 388 |
|
| 389 |
# Reducir contexto enviado: solo los 3 campos clave, NO la ficha técnica completa
|
| 390 |
try:
|
|
@@ -461,7 +489,7 @@ def procesar_correos_background(username: str, servidor_imap: str, licitacion_ac
|
|
| 461 |
|
| 462 |
try:
|
| 463 |
time.sleep(6) # 6s entre llamadas — respeta 15 RPM de Gemini Free
|
| 464 |
-
res_ia =
|
| 465 |
correos_enviados_a_gemini += 1
|
| 466 |
texto_ia = res_ia.text.strip().replace("```json", "").replace("```", "").strip()
|
| 467 |
datos_ia = json.loads(texto_ia)
|
|
@@ -557,14 +585,12 @@ def generar_ficha(
|
|
| 557 |
action="generar_ficha_cache",
|
| 558 |
licitacion=licitacion,
|
| 559 |
provider="gemini",
|
| 560 |
-
model=
|
| 561 |
metadata={"codigo_renglon": codigo_renglon}
|
| 562 |
)
|
| 563 |
return {"status": "success", "datasheet_md": cached, "from_cache": True}
|
| 564 |
|
| 565 |
try:
|
| 566 |
-
genai.configure(api_key=gemini_key, transport="rest")
|
| 567 |
-
model = genai.GenerativeModel('gemini-2.5-flash')
|
| 568 |
prompt = f"""Eres un Ingeniero de Compras especializado. Genera una ficha técnica en formato Markdown para el artículo: {codigo_renglon}.
|
| 569 |
Condiciones del Pliego: {pliego_context}
|
| 570 |
Detalle del Ítem: {items_context}
|
|
@@ -576,7 +602,7 @@ La ficha debe contener:
|
|
| 576 |
- **Condiciones especiales de la licitación**
|
| 577 |
Formato profesional y estructurado."""
|
| 578 |
|
| 579 |
-
response =
|
| 580 |
datasheet = response.text
|
| 581 |
|
| 582 |
# Guardar en cache para futuras consultas
|
|
@@ -585,7 +611,7 @@ Formato profesional y estructurado."""
|
|
| 585 |
username=username,
|
| 586 |
action="generar_ficha",
|
| 587 |
licitacion=licitacion,
|
| 588 |
-
model=
|
| 589 |
usage_metadata=response.usage_metadata,
|
| 590 |
metadata={"codigo_renglon": codigo_renglon}
|
| 591 |
)
|
|
@@ -600,7 +626,7 @@ Formato profesional y estructurado."""
|
|
| 600 |
action="generar_ficha",
|
| 601 |
licitacion=licitacion,
|
| 602 |
provider="gemini",
|
| 603 |
-
model=
|
| 604 |
status="error",
|
| 605 |
error_message=str(e)[:500],
|
| 606 |
metadata={"codigo_renglon": codigo_renglon}
|
|
|
|
| 17 |
os.environ.setdefault("GRPC_DEFAULT_SSL_ROOTS_FILE_PATH", CERTIFI_CA_BUNDLE)
|
| 18 |
except Exception:
|
| 19 |
pass
|
|
|
|
| 20 |
import json
|
| 21 |
import imaplib
|
| 22 |
import email
|
|
|
|
| 34 |
from bs4 import BeautifulSoup
|
| 35 |
import sys
|
| 36 |
import asyncio
|
| 37 |
+
import crypto
|
| 38 |
|
| 39 |
if sys.platform == "win32":
|
| 40 |
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
| 41 |
+
load_dotenv()
|
| 42 |
+
|
| 43 |
+
GEMINI_MODEL = "gemini-2.5-flash"
|
| 44 |
+
|
| 45 |
+
def get_gemini_client(api_key):
|
| 46 |
+
from google import genai
|
| 47 |
+
return genai.Client(api_key=str(api_key or "").strip())
|
| 48 |
+
|
| 49 |
+
def gemini_generate_content(api_key, contents, response_mime_type=None):
|
| 50 |
+
from google.genai import types
|
| 51 |
+
|
| 52 |
+
client = get_gemini_client(api_key)
|
| 53 |
+
config = None
|
| 54 |
+
if response_mime_type:
|
| 55 |
+
config = types.GenerateContentConfig(response_mime_type=response_mime_type)
|
| 56 |
+
kwargs = {"model": GEMINI_MODEL, "contents": contents}
|
| 57 |
+
if config:
|
| 58 |
+
kwargs["config"] = config
|
| 59 |
+
return client.models.generate_content(**kwargs)
|
| 60 |
+
|
| 61 |
+
def gemini_upload_file(client, path):
|
| 62 |
+
return client.files.upload(file=path)
|
| 63 |
+
|
| 64 |
+
def gemini_delete_file(client, uploaded_file):
|
| 65 |
+
try:
|
| 66 |
+
file_name = getattr(uploaded_file, "name", None)
|
| 67 |
+
if file_name:
|
| 68 |
+
client.files.delete(name=file_name)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
logger.warning(f"No se pudo borrar archivo temporal de Gemini: {e}")
|
| 71 |
|
| 72 |
# --- 1. LOGGING CON ROTACIÓN (max 2MB, 3 backups) ---
|
| 73 |
log_handler = RotatingFileHandler('backend.log', maxBytes=2*1024*1024, backupCount=3, encoding='utf-8')
|
|
|
|
| 322 |
started_at = time.perf_counter()
|
| 323 |
api_key_clean = gemini_key.strip()
|
| 324 |
try:
|
| 325 |
+
client = get_gemini_client(api_key_clean)
|
| 326 |
+
archivos_subidos = []
|
| 327 |
|
| 328 |
for archivo in archivos_pdf:
|
| 329 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp:
|
|
|
|
| 331 |
tmp.write(content)
|
| 332 |
tmp_path = tmp.name
|
| 333 |
|
| 334 |
+
uploaded_file = gemini_upload_file(client, tmp_path)
|
| 335 |
archivos_subidos.append(uploaded_file)
|
| 336 |
os.remove(tmp_path)
|
| 337 |
|
| 338 |
# gemini-2.5-flash para análisis complejo de PDFs
|
| 339 |
+
from google.genai import types
|
| 340 |
+
response = client.models.generate_content(
|
| 341 |
+
model=GEMINI_MODEL,
|
| 342 |
+
contents=[PROMPT_ANALISTA_MULTI, *archivos_subidos],
|
| 343 |
+
config=types.GenerateContentConfig(response_mime_type="application/json"),
|
| 344 |
+
)
|
| 345 |
try:
|
| 346 |
db.log_ai_usage(
|
| 347 |
username=username,
|
| 348 |
role=role,
|
| 349 |
action="analizar_pliego",
|
| 350 |
+
model=GEMINI_MODEL,
|
| 351 |
usage_metadata=response.usage_metadata,
|
| 352 |
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
| 353 |
metadata={"pdf_count": len(archivos_subidos)}
|
|
|
|
| 356 |
logger.warning(f"Error logging metric: {e}")
|
| 357 |
|
| 358 |
|
| 359 |
+
# Limpieza de archivos en la nube de Gemini para evitar llenar la cuota
|
| 360 |
+
for f in archivos_subidos:
|
| 361 |
+
gemini_delete_file(client, f)
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
logger.info(f"{len(archivos_subidos)} pliego(s) analizados exitosamente.")
|
| 364 |
data = json.loads(response.text)
|
|
|
|
| 372 |
module="ai",
|
| 373 |
action="analizar_pliego",
|
| 374 |
provider="gemini",
|
| 375 |
+
model=GEMINI_MODEL,
|
| 376 |
status="error",
|
| 377 |
error_message=str(e)[:500],
|
| 378 |
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
|
|
| 411 |
return
|
| 412 |
|
| 413 |
try:
|
| 414 |
+
client = get_gemini_client(gemini_key)
|
| 415 |
# gemini-2.5-flash para clasificación simple de correos
|
|
|
|
| 416 |
|
| 417 |
# Reducir contexto enviado: solo los 3 campos clave, NO la ficha técnica completa
|
| 418 |
try:
|
|
|
|
| 489 |
|
| 490 |
try:
|
| 491 |
time.sleep(6) # 6s entre llamadas — respeta 15 RPM de Gemini Free
|
| 492 |
+
res_ia = client.models.generate_content(model=GEMINI_MODEL, contents=prompt)
|
| 493 |
correos_enviados_a_gemini += 1
|
| 494 |
texto_ia = res_ia.text.strip().replace("```json", "").replace("```", "").strip()
|
| 495 |
datos_ia = json.loads(texto_ia)
|
|
|
|
| 585 |
action="generar_ficha_cache",
|
| 586 |
licitacion=licitacion,
|
| 587 |
provider="gemini",
|
| 588 |
+
model=GEMINI_MODEL,
|
| 589 |
metadata={"codigo_renglon": codigo_renglon}
|
| 590 |
)
|
| 591 |
return {"status": "success", "datasheet_md": cached, "from_cache": True}
|
| 592 |
|
| 593 |
try:
|
|
|
|
|
|
|
| 594 |
prompt = f"""Eres un Ingeniero de Compras especializado. Genera una ficha técnica en formato Markdown para el artículo: {codigo_renglon}.
|
| 595 |
Condiciones del Pliego: {pliego_context}
|
| 596 |
Detalle del Ítem: {items_context}
|
|
|
|
| 602 |
- **Condiciones especiales de la licitación**
|
| 603 |
Formato profesional y estructurado."""
|
| 604 |
|
| 605 |
+
response = gemini_generate_content(gemini_key, prompt)
|
| 606 |
datasheet = response.text
|
| 607 |
|
| 608 |
# Guardar en cache para futuras consultas
|
|
|
|
| 611 |
username=username,
|
| 612 |
action="generar_ficha",
|
| 613 |
licitacion=licitacion,
|
| 614 |
+
model=GEMINI_MODEL,
|
| 615 |
usage_metadata=response.usage_metadata,
|
| 616 |
metadata={"codigo_renglon": codigo_renglon}
|
| 617 |
)
|
|
|
|
| 626 |
action="generar_ficha",
|
| 627 |
licitacion=licitacion,
|
| 628 |
provider="gemini",
|
| 629 |
+
model=GEMINI_MODEL,
|
| 630 |
status="error",
|
| 631 |
error_message=str(e)[:500],
|
| 632 |
metadata={"codigo_renglon": codigo_renglon}
|
app.py
CHANGED
|
@@ -85,6 +85,7 @@ def extraer_meta_nota(nota):
|
|
| 85 |
API_URL_BASE = os.getenv("API_URL_BASE", "http://localhost:8000/api/v1")
|
| 86 |
API_HEADERS = {"X-Internal-Token": os.getenv("INTERNAL_API_TOKEN", "default-dev-token")}
|
| 87 |
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "").strip()
|
|
|
|
| 88 |
TIEMPO_BLOQUEO = 15 # minutos — debe coincidir con database.py
|
| 89 |
APP_SESSION_TTL_SECONDS = int(os.getenv("APP_SESSION_TTL_SECONDS", "43200"))
|
| 90 |
SESSION_SECRET = (
|
|
@@ -101,9 +102,16 @@ MAPA_ESTADOS_SLI = {
|
|
| 101 |
"DESIERTA": "Desierta",
|
| 102 |
"CERRADA": "Oferta Enviada al SLI",
|
| 103 |
"ABIERTA": "En Preparacion",
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# --- 2. BASE DE DATOS LOCAL Y PERSISTENCIA ---
|
| 108 |
@st.cache_resource(show_spinner=False)
|
| 109 |
def init_db():
|
|
@@ -3297,11 +3305,7 @@ if active_view == "🏛️ Monitor ACP":
|
|
| 3297 |
else:
|
| 3298 |
with st.spinner("🎓 El Asesor Carlos Méndez está evaluando el estatus..."):
|
| 3299 |
try:
|
| 3300 |
-
|
| 3301 |
-
genai.configure(api_key=st.session_state.gemini_key, transport="rest")
|
| 3302 |
-
modelo_seg = genai.GenerativeModel('gemini-2.5-flash')
|
| 3303 |
-
|
| 3304 |
-
hist_seg_str = ""
|
| 3305 |
df_hist_seg = get_historial_seguimiento_cached(lic_id)
|
| 3306 |
if not df_hist_seg.empty:
|
| 3307 |
hist_seg_str = "\n".join([f"- {h['fecha'][:10]}: {h['estado_nuevo']} ({h['nota'] or 'Sin nota'})" for _, h in df_hist_seg.head(5).iterrows()])
|
|
@@ -3346,8 +3350,7 @@ Estructura tu respuesta en este formato exacto:
|
|
| 3346 |
|
| 3347 |
Sé directo y profesional, escribe máximo 100 palabras en total. ¡Usa tu amplia experiencia con la ACP!"""
|
| 3348 |
|
| 3349 |
-
|
| 3350 |
-
st.session_state[f"sli_analisis_ia_{lic_id}"] = resp_seg_ai.text.strip()
|
| 3351 |
except Exception as e:
|
| 3352 |
st.error(f"❌ Error al consultar al Asesor: {e}")
|
| 3353 |
|
|
@@ -4022,11 +4025,7 @@ if active_view == "🚀 Tablero de Operaciones":
|
|
| 4022 |
else:
|
| 4023 |
with st.spinner("📝 Gemini está redactando el RFQ profesional..."):
|
| 4024 |
try:
|
| 4025 |
-
|
| 4026 |
-
genai.configure(api_key=st.session_state.gemini_key, transport="rest")
|
| 4027 |
-
modelo_rfq = genai.GenerativeModel('gemini-2.5-flash')
|
| 4028 |
-
|
| 4029 |
-
cg = cg_render
|
| 4030 |
idioma = "English" if "Inglés" in idioma_rfq else "Español"
|
| 4031 |
|
| 4032 |
# Filtrar renglones según selección
|
|
@@ -4082,8 +4081,7 @@ Generate a complete, professional RFQ email body following EXACTLY this structur
|
|
| 4082 |
|
| 4083 |
Use professional business English/Spanish. Format tables using plain text dashes and pipes (ASCII art tables, NOT markdown) since this will go into an email."""
|
| 4084 |
|
| 4085 |
-
|
| 4086 |
-
cuerpo_rfq = resp_rfq.text.strip()
|
| 4087 |
|
| 4088 |
# Guardar en session state para poder editar y descargar
|
| 4089 |
st.session_state['rfq_generado'] = cuerpo_rfq
|
|
@@ -4300,11 +4298,7 @@ Use professional business English/Spanish. Format tables using plain text dashes
|
|
| 4300 |
else:
|
| 4301 |
with st.spinner("🎓 El Asesor Senior está analizando tu situación..."):
|
| 4302 |
try:
|
| 4303 |
-
|
| 4304 |
-
genai.configure(api_key=st.session_state.gemini_key, transport="rest")
|
| 4305 |
-
modelo_neg = genai.GenerativeModel('gemini-2.5-flash')
|
| 4306 |
-
|
| 4307 |
-
cg_ctx = st.session_state.get('cg', {})
|
| 4308 |
contexto_items = df_render[['renglon','codigo_articulo','cantidad','termino_de_busqueda_corto']].to_string(index=False) if not df_render.empty else "Sin renglones cargados."
|
| 4309 |
ref_precio_str = f"USD {precio_ref:,.2f}" if precio_ref > 0 else "No proporcionado"
|
| 4310 |
|
|
@@ -4350,8 +4344,7 @@ Responde con el siguiente formato estructurado en Markdown:
|
|
| 4350 |
---
|
| 4351 |
*💬 Tip del Asesor: [Un consejo de oro corto y memorable basado en experiencia real de negociaciones]*"""
|
| 4352 |
|
| 4353 |
-
|
| 4354 |
-
analisis = resp_asesor.text.strip()
|
| 4355 |
|
| 4356 |
st.markdown("""
|
| 4357 |
<div style="background:#0D1117; border:1px solid #238636; border-radius:8px;
|
|
@@ -4426,11 +4419,7 @@ Responde con el siguiente formato estructurado en Markdown:
|
|
| 4426 |
with st.chat_message("assistant"):
|
| 4427 |
with st.spinner("Analizando..."):
|
| 4428 |
try:
|
| 4429 |
-
|
| 4430 |
-
genai.configure(api_key=st.session_state.gemini_key, transport="rest")
|
| 4431 |
-
modelo_cop = genai.GenerativeModel('gemini-2.5-flash')
|
| 4432 |
-
|
| 4433 |
-
# Construir contexto de la licitación activa
|
| 4434 |
cg_ctx = st.session_state.get('cg', {})
|
| 4435 |
items_ctx = df_render[['renglon','codigo_articulo','cantidad','unidad_de_medida','termino_de_busqueda_corto']].to_string(index=False) if not df_render.empty else "Sin renglones cargados."
|
| 4436 |
|
|
@@ -4459,8 +4448,7 @@ PREGUNTA DEL USUARIO:
|
|
| 4459 |
|
| 4460 |
Responde de forma clara y profesional. Si puedes dar un número o dato exacto del contexto, hazlo."""
|
| 4461 |
|
| 4462 |
-
|
| 4463 |
-
respuesta = resp_cop.text.strip()
|
| 4464 |
st.markdown(respuesta)
|
| 4465 |
copilot_messages.append({"role": "assistant", "content": respuesta})
|
| 4466 |
|
|
|
|
| 85 |
API_URL_BASE = os.getenv("API_URL_BASE", "http://localhost:8000/api/v1")
|
| 86 |
API_HEADERS = {"X-Internal-Token": os.getenv("INTERNAL_API_TOKEN", "default-dev-token")}
|
| 87 |
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "").strip()
|
| 88 |
+
GEMINI_MODEL = "gemini-2.5-flash"
|
| 89 |
TIEMPO_BLOQUEO = 15 # minutos — debe coincidir con database.py
|
| 90 |
APP_SESSION_TTL_SECONDS = int(os.getenv("APP_SESSION_TTL_SECONDS", "43200"))
|
| 91 |
SESSION_SECRET = (
|
|
|
|
| 102 |
"DESIERTA": "Desierta",
|
| 103 |
"CERRADA": "Oferta Enviada al SLI",
|
| 104 |
"ABIERTA": "En Preparacion",
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
def gemini_generate_text(api_key, prompt):
|
| 108 |
+
from google import genai
|
| 109 |
+
|
| 110 |
+
client = genai.Client(api_key=str(api_key or "").strip())
|
| 111 |
+
response = client.models.generate_content(model=GEMINI_MODEL, contents=prompt)
|
| 112 |
+
return str(getattr(response, "text", "") or "").strip()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
# --- 2. BASE DE DATOS LOCAL Y PERSISTENCIA ---
|
| 116 |
@st.cache_resource(show_spinner=False)
|
| 117 |
def init_db():
|
|
|
|
| 3305 |
else:
|
| 3306 |
with st.spinner("🎓 El Asesor Carlos Méndez está evaluando el estatus..."):
|
| 3307 |
try:
|
| 3308 |
+
hist_seg_str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3309 |
df_hist_seg = get_historial_seguimiento_cached(lic_id)
|
| 3310 |
if not df_hist_seg.empty:
|
| 3311 |
hist_seg_str = "\n".join([f"- {h['fecha'][:10]}: {h['estado_nuevo']} ({h['nota'] or 'Sin nota'})" for _, h in df_hist_seg.head(5).iterrows()])
|
|
|
|
| 3350 |
|
| 3351 |
Sé directo y profesional, escribe máximo 100 palabras en total. ¡Usa tu amplia experiencia con la ACP!"""
|
| 3352 |
|
| 3353 |
+
st.session_state[f"sli_analisis_ia_{lic_id}"] = gemini_generate_text(st.session_state.gemini_key, prompt_seg_ai)
|
|
|
|
| 3354 |
except Exception as e:
|
| 3355 |
st.error(f"❌ Error al consultar al Asesor: {e}")
|
| 3356 |
|
|
|
|
| 4025 |
else:
|
| 4026 |
with st.spinner("📝 Gemini está redactando el RFQ profesional..."):
|
| 4027 |
try:
|
| 4028 |
+
cg = cg_render
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4029 |
idioma = "English" if "Inglés" in idioma_rfq else "Español"
|
| 4030 |
|
| 4031 |
# Filtrar renglones según selección
|
|
|
|
| 4081 |
|
| 4082 |
Use professional business English/Spanish. Format tables using plain text dashes and pipes (ASCII art tables, NOT markdown) since this will go into an email."""
|
| 4083 |
|
| 4084 |
+
cuerpo_rfq = gemini_generate_text(st.session_state.gemini_key, prompt_rfq)
|
|
|
|
| 4085 |
|
| 4086 |
# Guardar en session state para poder editar y descargar
|
| 4087 |
st.session_state['rfq_generado'] = cuerpo_rfq
|
|
|
|
| 4298 |
else:
|
| 4299 |
with st.spinner("🎓 El Asesor Senior está analizando tu situación..."):
|
| 4300 |
try:
|
| 4301 |
+
cg_ctx = st.session_state.get('cg', {})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4302 |
contexto_items = df_render[['renglon','codigo_articulo','cantidad','termino_de_busqueda_corto']].to_string(index=False) if not df_render.empty else "Sin renglones cargados."
|
| 4303 |
ref_precio_str = f"USD {precio_ref:,.2f}" if precio_ref > 0 else "No proporcionado"
|
| 4304 |
|
|
|
|
| 4344 |
---
|
| 4345 |
*💬 Tip del Asesor: [Un consejo de oro corto y memorable basado en experiencia real de negociaciones]*"""
|
| 4346 |
|
| 4347 |
+
analisis = gemini_generate_text(st.session_state.gemini_key, prompt_asesor)
|
|
|
|
| 4348 |
|
| 4349 |
st.markdown("""
|
| 4350 |
<div style="background:#0D1117; border:1px solid #238636; border-radius:8px;
|
|
|
|
| 4419 |
with st.chat_message("assistant"):
|
| 4420 |
with st.spinner("Analizando..."):
|
| 4421 |
try:
|
| 4422 |
+
# Construir contexto de la licitación activa
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4423 |
cg_ctx = st.session_state.get('cg', {})
|
| 4424 |
items_ctx = df_render[['renglon','codigo_articulo','cantidad','unidad_de_medida','termino_de_busqueda_corto']].to_string(index=False) if not df_render.empty else "Sin renglones cargados."
|
| 4425 |
|
|
|
|
| 4448 |
|
| 4449 |
Responde de forma clara y profesional. Si puedes dar un número o dato exacto del contexto, hazlo."""
|
| 4450 |
|
| 4451 |
+
respuesta = gemini_generate_text(st.session_state.gemini_key, prompt_cop)
|
|
|
|
| 4452 |
st.markdown(respuesta)
|
| 4453 |
copilot_messages.append({"role": "assistant", "content": respuesta})
|
| 4454 |
|
requirements.txt
CHANGED
|
@@ -2,7 +2,7 @@ streamlit
|
|
| 2 |
pandas
|
| 3 |
plotly
|
| 4 |
tavily-python
|
| 5 |
-
google-
|
| 6 |
cryptography
|
| 7 |
requests
|
| 8 |
fastapi
|
|
|
|
| 2 |
pandas
|
| 3 |
plotly
|
| 4 |
tavily-python
|
| 5 |
+
google-genai
|
| 6 |
cryptography
|
| 7 |
requests
|
| 8 |
fastapi
|