tokiospg commited on
Commit
8dee463
·
verified ·
1 Parent(s): 35d48c7

Upload 3 files

Browse files
Files changed (3) hide show
  1. api.py +51 -25
  2. app.py +19 -31
  3. 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
- genai.configure(api_key=api_key_clean, transport="rest")
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 = genai.upload_file(path=tmp_path, mime_type="application/pdf")
307
  archivos_subidos.append(uploaded_file)
308
  os.remove(tmp_path)
309
 
310
  # gemini-2.5-flash para análisis complejo de PDFs
311
- model = genai.GenerativeModel('gemini-2.5-flash', generation_config={"response_mime_type": "application/json"})
312
- response = model.generate_content([PROMPT_ANALISTA_MULTI, *archivos_subidos])
 
 
 
 
313
  try:
314
  db.log_ai_usage(
315
  username=username,
316
  role=role,
317
  action="analizar_pliego",
318
- model="gemini-2.5-flash",
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
- try:
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="gemini-2.5-flash",
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
- genai.configure(api_key=gemini_key, transport="rest")
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 = model.generate_content(prompt)
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="gemini-2.5-flash",
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 = model.generate_content(prompt)
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="gemini-2.5-flash",
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="gemini-2.5-flash",
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
- import google.generativeai as genai
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
- resp_seg_ai = modelo_seg.generate_content(prompt_seg_ai)
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
- import google.generativeai as genai
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
- resp_rfq = modelo_rfq.generate_content(prompt_rfq)
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
- import google.generativeai as genai
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
- resp_asesor = modelo_neg.generate_content(prompt_asesor)
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
- import google.generativeai as genai
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
- resp_cop = modelo_cop.generate_content(prompt_cop)
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-generativeai
6
  cryptography
7
  requests
8
  fastapi
 
2
  pandas
3
  plotly
4
  tavily-python
5
+ google-genai
6
  cryptography
7
  requests
8
  fastapi