Spaces:
Runtime error
Runtime error
Upload app.py
Browse files
app.py
CHANGED
|
@@ -56,6 +56,13 @@ def encrypt_data(text):
|
|
| 56 |
def decrypt_data(text):
|
| 57 |
return crypto.decrypt_data(text)
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
API_URL_BASE = os.getenv("API_URL_BASE", "http://localhost:8000/api/v1")
|
| 60 |
API_HEADERS = {"X-Internal-Token": os.getenv("INTERNAL_API_TOKEN", "default-dev-token")}
|
| 61 |
TIEMPO_BLOQUEO = 15 # minutos — debe coincidir con database.py
|
|
@@ -650,6 +657,87 @@ with tab_acp:
|
|
| 650 |
</div>
|
| 651 |
""", unsafe_allow_html=True)
|
| 652 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
# Controles — fila 1: manual
|
| 654 |
resumen_acta_cache = st.session_state.get(f"sli_resumen_acta_{lic_id}")
|
| 655 |
if resumen_acta_cache:
|
|
@@ -707,6 +795,24 @@ with tab_acp:
|
|
| 707 |
# Mapear estatus SLI al catálogo interno
|
| 708 |
estado_mapeado = MAPA_ESTADOS_SLI.get(
|
| 709 |
estatus_sli.upper(), estatus_sli)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
nota_auto = f"[SLI Auto] Estatus: {estatus_sli}"
|
| 711 |
if cierre_sli:
|
| 712 |
nota_auto += f" | Cierre: {cierre_sli}"
|
|
@@ -768,6 +874,102 @@ with tab_acp:
|
|
| 768 |
st.toast(f"Seguimiento {num_lic} eliminado.", icon="🗑️")
|
| 769 |
st.rerun()
|
| 770 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
|
| 772 |
# Historial colapsable
|
| 773 |
df_hist_seg = db.get_historial_seguimiento(lic_id)
|
|
@@ -1457,12 +1659,17 @@ Responde con el siguiente formato estructurado en Markdown:
|
|
| 1457 |
</p>
|
| 1458 |
""", unsafe_allow_html=True)
|
| 1459 |
|
| 1460 |
-
# Inicializar historial
|
| 1461 |
-
|
| 1462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1463 |
|
| 1464 |
# Mostrar historial
|
| 1465 |
-
for msg in
|
| 1466 |
with st.chat_message(msg["role"]):
|
| 1467 |
st.markdown(msg["content"])
|
| 1468 |
|
|
@@ -1474,7 +1681,7 @@ Responde con el siguiente formato estructurado en Markdown:
|
|
| 1474 |
st.error("🔑 Configura tu API Key de Gemini en el panel lateral.")
|
| 1475 |
else:
|
| 1476 |
# Añadir mensaje del usuario
|
| 1477 |
-
|
| 1478 |
with st.chat_message("user"):
|
| 1479 |
st.markdown(pregunta)
|
| 1480 |
|
|
@@ -1491,7 +1698,7 @@ Responde con el siguiente formato estructurado en Markdown:
|
|
| 1491 |
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."
|
| 1492 |
|
| 1493 |
# Historial reciente del chat (últimas 6 interacciones)
|
| 1494 |
-
historial_str = "\n".join([f"{m['role'].upper()}: {m['content']}" for m in
|
| 1495 |
|
| 1496 |
prompt_cop = f"""Eres Procura Copilot, un asistente experto en procura y compras industriales de la empresa Proyelec S.A.
|
| 1497 |
Respondes en español de forma concisa, directa y útil. Si no tienes datos suficientes, lo dices honestamente.
|
|
@@ -1518,17 +1725,17 @@ Responde de forma clara y profesional. Si puedes dar un número o dato exacto de
|
|
| 1518 |
resp_cop = modelo_cop.generate_content(prompt_cop)
|
| 1519 |
respuesta = resp_cop.text.strip()
|
| 1520 |
st.markdown(respuesta)
|
| 1521 |
-
|
| 1522 |
|
| 1523 |
except Exception as e:
|
| 1524 |
err_msg = f"❌ Error del Copilot: {e}"
|
| 1525 |
st.error(err_msg)
|
| 1526 |
-
|
| 1527 |
|
| 1528 |
# Botón para limpiar chat
|
| 1529 |
-
if
|
| 1530 |
if st.button("🗑️ Limpiar conversación", key="clear_copilot"):
|
| 1531 |
-
st.session_state.
|
| 1532 |
st.rerun()
|
| 1533 |
|
| 1534 |
|
|
|
|
| 56 |
def decrypt_data(text):
|
| 57 |
return crypto.decrypt_data(text)
|
| 58 |
|
| 59 |
+
def extraer_meta_nota(nota):
|
| 60 |
+
cierre_match = re.search(r"Cierre:\s*([^|]+)", nota)
|
| 61 |
+
rev_match = re.search(r"Última rev:\s*([^|]+)", nota)
|
| 62 |
+
cierre = cierre_match.group(1).strip() if cierre_match else None
|
| 63 |
+
rev = rev_match.group(1).strip() if rev_match else None
|
| 64 |
+
return cierre, rev
|
| 65 |
+
|
| 66 |
API_URL_BASE = os.getenv("API_URL_BASE", "http://localhost:8000/api/v1")
|
| 67 |
API_HEADERS = {"X-Internal-Token": os.getenv("INTERNAL_API_TOKEN", "default-dev-token")}
|
| 68 |
TIEMPO_BLOQUEO = 15 # minutos — debe coincidir con database.py
|
|
|
|
| 657 |
</div>
|
| 658 |
""", unsafe_allow_html=True)
|
| 659 |
|
| 660 |
+
# --- ALERTA DE ENMIENDA RECIENTE ---
|
| 661 |
+
alerta_enmienda = st.session_state.get(f"sli_alerta_enmienda_{lic_id}")
|
| 662 |
+
if alerta_enmienda:
|
| 663 |
+
st.markdown(f"""
|
| 664 |
+
<div style="background: #2B220B; border: 1px solid #E3B341; border-left: 4px solid #E3B341;
|
| 665 |
+
padding: 10px 14px; border-radius: 8px; margin: 8px 0 12px 0;">
|
| 666 |
+
<div style="color: #F6B44B; font-weight: bold; font-size: 12px; margin-bottom: 4px; display: flex; align-items: center; gap: 6px;">
|
| 667 |
+
⚠️ Nueva Enmienda o Modificación Detectada en ACP
|
| 668 |
+
</div>
|
| 669 |
+
<div style="color: #E6EDF3; font-size: 11px; line-height: 1.45;">
|
| 670 |
+
El portal SLI registra una actualización en los documentos de esta licitación.<br>
|
| 671 |
+
• <b>Última Revisión:</b> {alerta_enmienda['rev_nueva']} (antes: {alerta_enmienda['rev_antigua'] or 'Desconocida'})<br>
|
| 672 |
+
• <b>Nueva Fecha de Cierre:</b> {alerta_enmienda['cierre_nuevo']} (antes: {alerta_enmienda['cierre_antiguo'] or 'Desconocida'})
|
| 673 |
+
</div>
|
| 674 |
+
</div>
|
| 675 |
+
""", unsafe_allow_html=True)
|
| 676 |
+
|
| 677 |
+
# --- LÍNEA DE TIEMPO INTERACTIVA / PROGRESO ---
|
| 678 |
+
estado_idx = 1
|
| 679 |
+
if estado == "Oferta Enviada al SLI":
|
| 680 |
+
estado_idx = 2
|
| 681 |
+
elif estado in ["Cumple Tecnicamente", "No Cumple Tecnicamente", "En Evaluacion Economica"]:
|
| 682 |
+
estado_idx = 3
|
| 683 |
+
elif estado in ["Adjudicada", "No Adjudicada", "Desierta"]:
|
| 684 |
+
estado_idx = 4
|
| 685 |
+
|
| 686 |
+
p1_bg = "#4F9CF9"
|
| 687 |
+
p1_txt = "#080C12"
|
| 688 |
+
p2_bg = "#F6B44B" if estado_idx >= 2 else "#242C38"
|
| 689 |
+
p2_txt = "#080C12" if estado_idx >= 2 else "#8C98A8"
|
| 690 |
+
p3_bg = "#F08A3C" if estado_idx >= 3 else "#242C38"
|
| 691 |
+
p3_txt = "#080C12" if estado_idx >= 3 else "#8C98A8"
|
| 692 |
+
|
| 693 |
+
if estado == "No Cumple Tecnicamente":
|
| 694 |
+
p3_bg = "#EF5B5B"
|
| 695 |
+
p3_txt = "#ffffff"
|
| 696 |
+
|
| 697 |
+
p4_bg = "#31C48D" if estado_idx == 4 else "#242C38"
|
| 698 |
+
p4_txt = "#080C12" if estado_idx == 4 else "#8C98A8"
|
| 699 |
+
|
| 700 |
+
if estado == "No Adjudicada":
|
| 701 |
+
p4_bg = "#748091"
|
| 702 |
+
p4_txt = "#ffffff"
|
| 703 |
+
elif estado == "Desierta":
|
| 704 |
+
p4_bg = "#748091"
|
| 705 |
+
p4_txt = "#ffffff"
|
| 706 |
+
|
| 707 |
+
l1_color = "#F6B44B" if estado_idx >= 2 else "#242C38"
|
| 708 |
+
l2_color = "#F08A3C" if estado_idx >= 3 else "#242C38"
|
| 709 |
+
l3_color = "#31C48D" if estado_idx == 4 else "#242C38"
|
| 710 |
+
|
| 711 |
+
st.markdown(f"""
|
| 712 |
+
<div style="display: flex; align-items: center; justify-content: space-between;
|
| 713 |
+
margin: 10px 0 16px 0; padding: 10px 14px; background: #161B22;
|
| 714 |
+
border-radius: 8px; border: 1px solid #30363D;">
|
| 715 |
+
<!-- Paso 1 -->
|
| 716 |
+
<div style="text-align: center; flex: 1;">
|
| 717 |
+
<div style="width: 22px; height: 22px; border-radius: 50%; background: {p1_bg}; color: {p1_txt}; display: flex; align-items: center; justify-content: center; margin: 0 auto 4px auto; font-size: 10px; font-weight: bold; box-shadow: 0 0 10px rgba(79, 156, 249, 0.2);">{ "✓" if estado_idx > 1 else "1" }</div>
|
| 718 |
+
<div style="font-size: 9px; color: #4F9CF9; font-weight: 700; text-transform: uppercase;">Preparación</div>
|
| 719 |
+
</div>
|
| 720 |
+
<div style="flex: 0.4; height: 2px; background: {l1_color}; margin-bottom: 12px;"></div>
|
| 721 |
+
<!-- Paso 2 -->
|
| 722 |
+
<div style="text-align: center; flex: 1;">
|
| 723 |
+
<div style="width: 22px; height: 22px; border-radius: 50%; background: {p2_bg}; color: {p2_txt}; display: flex; align-items: center; justify-content: center; margin: 0 auto 4px auto; font-size: 10px; font-weight: bold;">{ "✓" if estado_idx > 2 else "2" }</div>
|
| 724 |
+
<div style="font-size: 9px; color: { "#F6B44B" if estado_idx >= 2 else "#8C98A8" }; font-weight: 700; text-transform: uppercase;">Envío SLI</div>
|
| 725 |
+
</div>
|
| 726 |
+
<div style="flex: 0.4; height: 2px; background: {l2_color}; margin-bottom: 12px;"></div>
|
| 727 |
+
<!-- Paso 3 -->
|
| 728 |
+
<div style="text-align: center; flex: 1;">
|
| 729 |
+
<div style="width: 22px; height: 22px; border-radius: 50%; background: {p3_bg}; color: {p3_txt}; display: flex; align-items: center; justify-content: center; margin: 0 auto 4px auto; font-size: 10px; font-weight: bold;">{ "✓" if estado_idx > 3 else "3" }</div>
|
| 730 |
+
<div style="font-size: 9px; color: { "#F08A3C" if estado_idx >= 3 else "#8C98A8" }; font-weight: 700; text-transform: uppercase;">Evaluación</div>
|
| 731 |
+
</div>
|
| 732 |
+
<div style="flex: 0.4; height: 2px; background: {l3_color}; margin-bottom: 12px;"></div>
|
| 733 |
+
<!-- Paso 4 -->
|
| 734 |
+
<div style="text-align: center; flex: 1;">
|
| 735 |
+
<div style="width: 22px; height: 22px; border-radius: 50%; background: {p4_bg}; color: {p4_txt}; display: flex; align-items: center; justify-content: center; margin: 0 auto 4px auto; font-size: 10px; font-weight: bold;">{ "✓" if estado_idx == 4 else "4" }</div>
|
| 736 |
+
<div style="font-size: 9px; color: { "#31C48D" if estado_idx == 4 else "#8C98A8" }; font-weight: 700; text-transform: uppercase;">Resolución</div>
|
| 737 |
+
</div>
|
| 738 |
+
</div>
|
| 739 |
+
""", unsafe_allow_html=True)
|
| 740 |
+
|
| 741 |
# Controles — fila 1: manual
|
| 742 |
resumen_acta_cache = st.session_state.get(f"sli_resumen_acta_{lic_id}")
|
| 743 |
if resumen_acta_cache:
|
|
|
|
| 795 |
# Mapear estatus SLI al catálogo interno
|
| 796 |
estado_mapeado = MAPA_ESTADOS_SLI.get(
|
| 797 |
estatus_sli.upper(), estatus_sli)
|
| 798 |
+
|
| 799 |
+
# --- WATCHDOG DE ENMIENDAS / CAMBIOS ---
|
| 800 |
+
df_hist_prev = db.get_historial_seguimiento(lic_id)
|
| 801 |
+
if not df_hist_prev.empty:
|
| 802 |
+
nota_antigua = str(df_hist_prev.iloc[0].get('nota', ''))
|
| 803 |
+
cierre_antiguo, rev_antigua = extraer_meta_nota(nota_antigua)
|
| 804 |
+
|
| 805 |
+
cambio_cierre = cierre_sli and cierre_antiguo and (cierre_sli.strip() != cierre_antiguo.strip())
|
| 806 |
+
cambio_rev = rev_sli and rev_antigua and (rev_sli.strip() != rev_antigua.strip())
|
| 807 |
+
|
| 808 |
+
if cambio_cierre or cambio_rev:
|
| 809 |
+
st.session_state[f"sli_alerta_enmienda_{lic_id}"] = {
|
| 810 |
+
"cierre_antiguo": cierre_antiguo,
|
| 811 |
+
"cierre_nuevo": cierre_sli,
|
| 812 |
+
"rev_antigua": rev_antigua,
|
| 813 |
+
"rev_nueva": rev_sli
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
nota_auto = f"[SLI Auto] Estatus: {estatus_sli}"
|
| 817 |
if cierre_sli:
|
| 818 |
nota_auto += f" | Cierre: {cierre_sli}"
|
|
|
|
| 874 |
st.toast(f"Seguimiento {num_lic} eliminado.", icon="🗑️")
|
| 875 |
st.rerun()
|
| 876 |
|
| 877 |
+
# --- ASESORÍA ESTRATÉGICA IA ---
|
| 878 |
+
st.markdown("<div style='margin-top: 10px;'></div>", unsafe_allow_html=True)
|
| 879 |
+
btn_col, clear_btn_col = st.columns([0.7, 0.3])
|
| 880 |
+
with btn_col:
|
| 881 |
+
btn_consultar_ia = st.button(
|
| 882 |
+
"🎓 Carlos Méndez: Consultar Estrategia",
|
| 883 |
+
key=f"ai_carlos_{lic_id}",
|
| 884 |
+
use_container_width=True
|
| 885 |
+
)
|
| 886 |
+
with clear_btn_col:
|
| 887 |
+
btn_clear_ia = st.button(
|
| 888 |
+
"🗑️ Limpiar Asesoría",
|
| 889 |
+
key=f"clear_ai_carlos_{lic_id}",
|
| 890 |
+
use_container_width=True
|
| 891 |
+
)
|
| 892 |
+
|
| 893 |
+
# Si el usuario hace clic en Consultar
|
| 894 |
+
if btn_consultar_ia:
|
| 895 |
+
if not st.session_state.gemini_key:
|
| 896 |
+
st.error("🔑 Configura tu API Key de Gemini en el panel lateral.")
|
| 897 |
+
else:
|
| 898 |
+
with st.spinner("🎓 El Asesor Carlos Méndez está evaluando el estatus..."):
|
| 899 |
+
try:
|
| 900 |
+
import google.generativeai as genai
|
| 901 |
+
genai.configure(api_key=st.session_state.gemini_key)
|
| 902 |
+
modelo_seg = genai.GenerativeModel('gemini-2.0-flash')
|
| 903 |
+
|
| 904 |
+
hist_seg_str = ""
|
| 905 |
+
df_hist_seg = db.get_historial_seguimiento(lic_id)
|
| 906 |
+
if not df_hist_seg.empty:
|
| 907 |
+
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()])
|
| 908 |
+
else:
|
| 909 |
+
hist_seg_str = "Sin historial de actualizaciones."
|
| 910 |
+
|
| 911 |
+
alerta_ctx = ""
|
| 912 |
+
alerta_enmienda = st.session_state.get(f"sli_alerta_enmienda_{lic_id}")
|
| 913 |
+
if alerta_enmienda:
|
| 914 |
+
alerta_ctx = f"""
|
| 915 |
+
*** ALERTA DE ENMIENDA RECIENTE EN LA ACP ***
|
| 916 |
+
- Se detectó un cambio en el pliego o documentos adjuntos en la última consulta.
|
| 917 |
+
- Fecha de revisión anterior: {alerta_enmienda.get('rev_antigua','N/A')}
|
| 918 |
+
- Nueva fecha de revisión: {alerta_enmienda.get('rev_nueva','N/A')}
|
| 919 |
+
- Fecha de cierre anterior: {alerta_enmienda.get('cierre_antiguo','N/A')}
|
| 920 |
+
- Nueva fecha de cierre: {alerta_enmienda.get('cierre_nuevo','N/A')}
|
| 921 |
+
Debes comentar esta alerta en tu recomendación y dar instrucciones claras de cómo proceder."""
|
| 922 |
+
|
| 923 |
+
prompt_seg_ai = f"""Eres Carlos Méndez, Asesor Senior de Procura con 25 años de experiencia en licitaciones del Canal de Panamá (ACP) y Procura B2B.
|
| 924 |
+
|
| 925 |
+
El analista te pide tu opinión experta sobre esta licitación activa en el Monitor ACP:
|
| 926 |
+
|
| 927 |
+
INFORMACIÓN DE LA LICITACIÓN:
|
| 928 |
+
- RFQ / Número: {num_lic}
|
| 929 |
+
- Objeto: {objeto_safe}
|
| 930 |
+
- Responsable: {resp_safe}
|
| 931 |
+
- Estado Actual: {estado}
|
| 932 |
+
- Días desde que se envió la oferta: {dias_txt or "No definido"}
|
| 933 |
+
- Monto Ofertado: {moneda} {monto:,.2f}
|
| 934 |
+
|
| 935 |
+
HISTORIAL DE ACTUALIZACIONES RECIENTES:
|
| 936 |
+
{hist_seg_str}
|
| 937 |
+
{alerta_ctx}
|
| 938 |
+
|
| 939 |
+
Tu misión es dar un análisis estratégico súper corto, ultra-accionable y con lenguaje de mentor (en español).
|
| 940 |
+
|
| 941 |
+
Estructura tu respuesta en este formato exacto:
|
| 942 |
+
🎓 **Estrategia para Licitación {num_lic}**
|
| 943 |
+
- **Diagnóstico del Estatus**: [1 frase sobre qué significa esta fase o el retraso actual en la ACP, comentando brevemente la enmienda si existe]
|
| 944 |
+
- **Acción Inmediata**: [1 paso concreto que el analista debe hacer HOY. Ej: Extender fianza de propuesta, preparar subsanación técnica o monitorear acta de apertura]
|
| 945 |
+
- **Alerta de Riesgo**: [Qué podría salir mal si no se actúa y cómo mitigarlo]
|
| 946 |
+
|
| 947 |
+
Sé directo y profesional, escribe máximo 100 palabras en total. ¡Usa tu amplia experiencia con la ACP!"""
|
| 948 |
+
|
| 949 |
+
resp_seg_ai = modelo_seg.generate_content(prompt_seg_ai)
|
| 950 |
+
st.session_state[f"sli_analisis_ia_{lic_id}"] = resp_seg_ai.text.strip()
|
| 951 |
+
except Exception as e:
|
| 952 |
+
st.error(f"❌ Error al consultar al Asesor: {e}")
|
| 953 |
+
|
| 954 |
+
# Si el usuario hace clic en Limpiar
|
| 955 |
+
if btn_clear_ia:
|
| 956 |
+
st.session_state.pop(f"sli_analisis_ia_{lic_id}", None)
|
| 957 |
+
st.rerun()
|
| 958 |
+
|
| 959 |
+
# Mostrar análisis si existe
|
| 960 |
+
analisis_ia = st.session_state.get(f"sli_analisis_ia_{lic_id}")
|
| 961 |
+
if analisis_ia:
|
| 962 |
+
st.markdown(f"""
|
| 963 |
+
<div style="background: #0E1624; border: 1px solid #1E2A3A; border-left: 3px solid #10B981;
|
| 964 |
+
padding: 12px; border-radius: 8px; margin-top: 10px; margin-bottom: 10px;">
|
| 965 |
+
<div style="color: #31C48D; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 5px;">
|
| 966 |
+
🎓 Consejo Estratégico de Carlos Méndez, CPSM
|
| 967 |
+
</div>
|
| 968 |
+
<div style="color: #E6EDF3; font-size: 12.5px; line-height: 1.45;">
|
| 969 |
+
{analisis_ia}
|
| 970 |
+
</div>
|
| 971 |
+
</div>
|
| 972 |
+
""", unsafe_allow_html=True)
|
| 973 |
|
| 974 |
# Historial colapsable
|
| 975 |
df_hist_seg = db.get_historial_seguimiento(lic_id)
|
|
|
|
| 1659 |
</p>
|
| 1660 |
""", unsafe_allow_html=True)
|
| 1661 |
|
| 1662 |
+
# Inicializar historial de chat por licitación
|
| 1663 |
+
licitacion_actual = str(cg_render.get('numero_licitacion', 'default'))
|
| 1664 |
+
if "copilot_messages_by_lic" not in st.session_state:
|
| 1665 |
+
st.session_state.copilot_messages_by_lic = {}
|
| 1666 |
+
if licitacion_actual not in st.session_state.copilot_messages_by_lic:
|
| 1667 |
+
st.session_state.copilot_messages_by_lic[licitacion_actual] = []
|
| 1668 |
+
|
| 1669 |
+
copilot_messages = st.session_state.copilot_messages_by_lic[licitacion_actual]
|
| 1670 |
|
| 1671 |
# Mostrar historial
|
| 1672 |
+
for msg in copilot_messages:
|
| 1673 |
with st.chat_message(msg["role"]):
|
| 1674 |
st.markdown(msg["content"])
|
| 1675 |
|
|
|
|
| 1681 |
st.error("🔑 Configura tu API Key de Gemini en el panel lateral.")
|
| 1682 |
else:
|
| 1683 |
# Añadir mensaje del usuario
|
| 1684 |
+
copilot_messages.append({"role": "user", "content": pregunta})
|
| 1685 |
with st.chat_message("user"):
|
| 1686 |
st.markdown(pregunta)
|
| 1687 |
|
|
|
|
| 1698 |
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."
|
| 1699 |
|
| 1700 |
# Historial reciente del chat (últimas 6 interacciones)
|
| 1701 |
+
historial_str = "\n".join([f"{m['role'].upper()}: {m['content']}" for m in copilot_messages[-6:]])
|
| 1702 |
|
| 1703 |
prompt_cop = f"""Eres Procura Copilot, un asistente experto en procura y compras industriales de la empresa Proyelec S.A.
|
| 1704 |
Respondes en español de forma concisa, directa y útil. Si no tienes datos suficientes, lo dices honestamente.
|
|
|
|
| 1725 |
resp_cop = modelo_cop.generate_content(prompt_cop)
|
| 1726 |
respuesta = resp_cop.text.strip()
|
| 1727 |
st.markdown(respuesta)
|
| 1728 |
+
copilot_messages.append({"role": "assistant", "content": respuesta})
|
| 1729 |
|
| 1730 |
except Exception as e:
|
| 1731 |
err_msg = f"❌ Error del Copilot: {e}"
|
| 1732 |
st.error(err_msg)
|
| 1733 |
+
copilot_messages.append({"role": "assistant", "content": err_msg})
|
| 1734 |
|
| 1735 |
# Botón para limpiar chat
|
| 1736 |
+
if copilot_messages:
|
| 1737 |
if st.button("🗑️ Limpiar conversación", key="clear_copilot"):
|
| 1738 |
+
st.session_state.copilot_messages_by_lic[licitacion_actual] = []
|
| 1739 |
st.rerun()
|
| 1740 |
|
| 1741 |
|