feat(whatsapp): QWEN primary AI backend, Hermes fallback, conversation history, vehicle persistence, demo prompts
- Add QWEN (qwen3.6) as primary AI backend with short system prompt - Hermes remains as fallback with 45s timeout - Increase QWEN timeout to 35s, max_tokens to 4000 - Add conversation history loading from whatsapp_messages (last 4 msgs) - Persist detected vehicle in whatsapp_sessions table - Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history - Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel - Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.) - Improve no-stock response: conversational with alternatives - Split search_query by | for multi-part lookups - Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
This commit is contained in:
@@ -1,29 +1,24 @@
|
||||
# /home/Autopartes/pos/tenant_db.py
|
||||
"""Tenant DB connection manager with pooling.
|
||||
"""Tenant DB connection manager.
|
||||
|
||||
Uses psycopg2.pool.ThreadedConnectionPool for both master and tenant DBs.
|
||||
Connections are returned to the pool on .close() via a thin wrapper —
|
||||
this keeps the rest of the codebase unchanged.
|
||||
Master DB: creates a fresh connection each time (very light load thanks to
|
||||
tenant_id → db_name cache, so we only hit master ~once per 5 min).
|
||||
Tenant DBs: use psycopg2.pool.ThreadedConnectionPool with maxconn=20.
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
import psycopg2
|
||||
from psycopg2 import pool
|
||||
from config import MASTER_DB_URL, TENANT_DB_URL_TEMPLATE
|
||||
|
||||
|
||||
# ─── Pools ─────────────────────────────────────
|
||||
_master_pool = None
|
||||
# ─── Tenant Pools ──────────────────────────────
|
||||
_tenant_pools = {}
|
||||
|
||||
|
||||
def _get_master_pool():
|
||||
"""Lazy-initialize master DB connection pool."""
|
||||
global _master_pool
|
||||
if _master_pool is None:
|
||||
_master_pool = pool.ThreadedConnectionPool(
|
||||
minconn=2, maxconn=20, dsn=MASTER_DB_URL
|
||||
)
|
||||
return _master_pool
|
||||
# ─── Tenant cache ──────────────────────────────
|
||||
_tenant_cache = {}
|
||||
_tenant_cache_ttl = 300
|
||||
_tenant_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
def _get_tenant_pool(db_name):
|
||||
@@ -37,6 +32,34 @@ def _get_tenant_pool(db_name):
|
||||
return _tenant_pools[db_name]
|
||||
|
||||
|
||||
def _resolve_tenant_db(tenant_id):
|
||||
"""Return db_name for tenant_id, using cache first."""
|
||||
now = time.time()
|
||||
with _tenant_cache_lock:
|
||||
entry = _tenant_cache.get(tenant_id)
|
||||
if entry and entry['expires'] > now:
|
||||
return entry['db_name']
|
||||
|
||||
# Cache miss or expired — query master DB with a fresh connection
|
||||
conn = psycopg2.connect(MASTER_DB_URL)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT db_name FROM tenants WHERE id = %s AND is_active = true",
|
||||
(tenant_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
db_name = row[0] if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if db_name:
|
||||
with _tenant_cache_lock:
|
||||
_tenant_cache[tenant_id] = {'db_name': db_name, 'expires': now + _tenant_cache_ttl}
|
||||
return db_name
|
||||
|
||||
|
||||
class _PooledConnection:
|
||||
"""Thin wrapper that delegates all attribute access to the real
|
||||
psycopg2 connection, but returns it to the pool on .close().
|
||||
@@ -52,19 +75,17 @@ class _PooledConnection:
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
# Rollback any aborted transaction before returning to pool.
|
||||
# Without this, failed transactions leave connections in
|
||||
# 'idle in transaction (aborted)' state, eventually exhausting
|
||||
# the pool.
|
||||
if self._conn:
|
||||
try:
|
||||
self._conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
self._pool.putconn(self._conn)
|
||||
self._pool.putconn(self._conn)
|
||||
except Exception:
|
||||
# If pool is already closed, fall back to real close
|
||||
self._conn.close()
|
||||
try:
|
||||
self._conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
@@ -76,27 +97,19 @@ class _PooledConnection:
|
||||
# ─── Public API ────────────────────────────────
|
||||
|
||||
def get_master_conn():
|
||||
"""Get a pooled connection to the master DB."""
|
||||
p = _get_master_pool()
|
||||
return _PooledConnection(p.getconn(), p)
|
||||
"""Get a direct connection to the master DB (no pool).
|
||||
|
||||
Caller MUST close() the connection when done.
|
||||
"""
|
||||
return psycopg2.connect(MASTER_DB_URL)
|
||||
|
||||
|
||||
def get_tenant_conn(tenant_id):
|
||||
"""Get a pooled connection to a tenant's DB."""
|
||||
master = get_master_conn()
|
||||
cur = master.cursor()
|
||||
cur.execute(
|
||||
"SELECT db_name FROM tenants WHERE id = %s AND is_active = true",
|
||||
(tenant_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
master.close()
|
||||
|
||||
if not row:
|
||||
db_name = _resolve_tenant_db(tenant_id)
|
||||
if not db_name:
|
||||
raise ValueError(f"Tenant {tenant_id} not found or inactive")
|
||||
|
||||
db_name = row[0]
|
||||
p = _get_tenant_pool(db_name)
|
||||
return _PooledConnection(p.getconn(), p)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user