mirror of
https://github.com/marcogll/telegram_expenses_controller.git
synced 2026-01-13 13:25:15 +00:00
feat: translate comments, docstrings, and log messages to Spanish.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
AI-powered classification and confidence scoring.
|
||||
Clasificación y puntuación de confianza impulsada por IA.
|
||||
"""
|
||||
import openai
|
||||
import json
|
||||
@@ -17,25 +17,25 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def classify_and_audit(expense: ProvisionalExpense) -> ProvisionalExpense:
|
||||
"""
|
||||
Uses an AI model to audit an extracted expense, providing a confidence
|
||||
score and notes. This is a placeholder for a more complex classification
|
||||
and validation logic.
|
||||
Utiliza un modelo de IA para auditar un gasto extraído, proporcionando una puntuación
|
||||
de confianza y notas. Este es un marcador de posición para una lógica de clasificación
|
||||
y validación más compleja.
|
||||
|
||||
Args:
|
||||
expense: A ProvisionalExpense object with extracted data.
|
||||
expense: Un objeto ProvisionalExpense con datos extraídos.
|
||||
|
||||
Returns:
|
||||
The same ProvisionalExpense object, updated with the audit findings.
|
||||
El mismo objeto ProvisionalExpense, actualizado con los hallazgos de la auditoría.
|
||||
"""
|
||||
logger.info(f"Starting AI audit for expense: {expense.extracted_data.description}")
|
||||
logger.info(f"Iniciando auditoría por IA para el gasto: {expense.extracted_data.description}")
|
||||
|
||||
# For now, this is a placeholder. A real implementation would
|
||||
# call an AI model like in the extractor.
|
||||
# For demonstration, we'll just assign a high confidence score.
|
||||
# Por ahora, esto es un marcador de posición. Una implementación real
|
||||
# llamaría a un modelo de IA como en el extractor.
|
||||
# Para la demostración, simplemente asignaremos una puntuación de confianza alta.
|
||||
|
||||
expense.confidence_score = 0.95
|
||||
expense.validation_notes.append("AI audit placeholder: auto-approved.")
|
||||
expense.processing_method = "ai_inference" # Assume AI was used
|
||||
expense.validation_notes.append("Marcador de posición de auditoría por IA: aprobado automáticamente.")
|
||||
expense.processing_method = "ai_inference" # Asumir que se usó IA
|
||||
|
||||
logger.info("AI audit placeholder complete.")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
AI-powered data extraction from raw text.
|
||||
Extracción de datos impulsada por IA a partir de texto sin procesar.
|
||||
"""
|
||||
import openai
|
||||
import json
|
||||
@@ -17,15 +17,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def extract_expense_data(text: str) -> ExtractedExpense:
|
||||
"""
|
||||
Uses an AI model to extract structured expense data from a raw text string.
|
||||
Utiliza un modelo de IA para extraer datos de gastos estructurados de una cadena de texto sin procesar.
|
||||
|
||||
Args:
|
||||
text: The raw text from user input, OCR, or transcription.
|
||||
text: El texto sin procesar de la entrada del usuario, OCR o transcripción.
|
||||
|
||||
Returns:
|
||||
An ExtractedExpense object with the data found by the AI.
|
||||
Un objeto ExtractedExpense con los datos encontrados por la IA.
|
||||
"""
|
||||
logger.info(f"Starting AI extraction for text: '{text[:100]}...'")
|
||||
logger.info(f"Iniciando extracción por IA para el texto: '{text[:100]}...'")
|
||||
|
||||
try:
|
||||
response = openai.ChatCompletion.create(
|
||||
@@ -38,23 +38,23 @@ def extract_expense_data(text: str) -> ExtractedExpense:
|
||||
response_format={"type": "json_object"}
|
||||
)
|
||||
|
||||
# The response from OpenAI should be a JSON string in the message content
|
||||
# La respuesta de OpenAI debería ser una cadena JSON en el contenido del mensaje
|
||||
json_response = response.choices[0].message['content']
|
||||
extracted_data = json.loads(json_response)
|
||||
|
||||
logger.info(f"AI extraction successful. Raw JSON: {extracted_data}")
|
||||
logger.info(f"Extracción por IA exitosa. JSON sin procesar: {extracted_data}")
|
||||
|
||||
# Add the original text to the model for audit purposes
|
||||
# Añadir el texto original al modelo para fines de auditoría
|
||||
extracted_data['raw_text'] = text
|
||||
|
||||
return ExtractedExpense(**extracted_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to decode JSON from AI response: {e}")
|
||||
# Return a model with only the raw text for manual review
|
||||
logger.error(f"Error al decodificar JSON de la respuesta de la IA: {e}")
|
||||
# Devolver un modelo con solo el texto sin procesar para revisión manual
|
||||
return ExtractedExpense(raw_text=text)
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred during AI extraction: {e}")
|
||||
# Return a model with only the raw text
|
||||
logger.error(f"Ocurrió un error inesperado durante la extracción por IA: {e}")
|
||||
# Devolver un modelo con solo el texto sin procesar
|
||||
return ExtractedExpense(raw_text=text)
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
"""
|
||||
Configuration loader.
|
||||
Cargador de configuración.
|
||||
|
||||
Loads environment variables from a .env file and makes them available as a Config object.
|
||||
Carga las variables de entorno desde un archivo .env y las pone a disposición como un objeto Config.
|
||||
"""
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file in the project root
|
||||
# Note: The path is relative to the file's location in the final `app` directory
|
||||
# Cargar variables de entorno desde el archivo .env en la raíz del proyecto
|
||||
# Nota: La ruta es relativa a la ubicación del archivo en el directorio final `app`
|
||||
dotenv_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env')
|
||||
if os.path.exists(dotenv_path):
|
||||
load_dotenv(dotenv_path)
|
||||
|
||||
class Config:
|
||||
"""
|
||||
Holds the application's configuration.
|
||||
Contiene la configuración de la aplicación.
|
||||
"""
|
||||
# Telegram Bot Token
|
||||
# Token del Bot de Telegram
|
||||
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN")
|
||||
|
||||
# OpenAI API Key
|
||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
|
||||
# Supergroup ID for the bot
|
||||
# ID del Supergrupo para el bot
|
||||
SUPERGROUP_ID = os.getenv("SUPERGROUP_ID")
|
||||
|
||||
# Database URL (e.g., "sqlite:///expenses.db")
|
||||
# URL de la Base de Datos (ej., "sqlite:///expenses.db")
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///../database.db")
|
||||
|
||||
# Log level
|
||||
# Nivel de registro
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
|
||||
# Create a single instance of the configuration
|
||||
# Crear una única instancia de la configuración
|
||||
config = Config()
|
||||
|
||||
64
app/main.py
64
app/main.py
@@ -1,17 +1,17 @@
|
||||
"""
|
||||
Application entry point.
|
||||
Punto de entrada de la aplicación.
|
||||
|
||||
Initializes the FastAPI application, sets up logging, database,
|
||||
and defines the main API endpoints.
|
||||
Inicializa la aplicación FastAPI, configura el registro, la base de datos
|
||||
y define los principales endpoints de la API.
|
||||
"""
|
||||
import logging
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# It's crucial to set up the config before other imports
|
||||
# Es crucial configurar la configuración antes de otras importaciones
|
||||
from app.config import config
|
||||
|
||||
# Now, set up logging based on the config
|
||||
# Ahora, configurar el registro basado en la configuración
|
||||
logging.basicConfig(level=config.LOG_LEVEL.upper())
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,46 +20,46 @@ from app.schema.base import RawInput
|
||||
from app.router import process_expense_input
|
||||
from app.persistence import repositories, db
|
||||
|
||||
# Create database tables on startup
|
||||
# This is simple, but for production, you'd use migrations (e.g., Alembic)
|
||||
# Crear tablas de base de datos al inicio
|
||||
# Esto es simple, pero para producción, usarías migraciones (ej. Alembic)
|
||||
repositories.create_tables()
|
||||
|
||||
# Initialize the FastAPI app
|
||||
# Inicializar la aplicación FastAPI
|
||||
app = FastAPI(
|
||||
title="Telegram Expenses Bot API",
|
||||
description="Processes and manages expense data from various sources.",
|
||||
title="API del Bot de Gastos de Telegram",
|
||||
description="Procesa y gestiona datos de gastos de diversas fuentes.",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
logger.info("Application startup complete.")
|
||||
logger.info(f"Log level is set to: {config.LOG_LEVEL.upper()}")
|
||||
logger.info("Inicio de la aplicación completado.")
|
||||
logger.info(f"El nivel de registro está establecido en: {config.LOG_LEVEL.upper()}")
|
||||
|
||||
@app.get("/", tags=["Status"])
|
||||
@app.get("/", tags=["Estado"])
|
||||
async def root():
|
||||
"""Health check endpoint."""
|
||||
return {"message": "Telegram Expenses Bot API is running."}
|
||||
"""Endpoint de verificación de salud."""
|
||||
return {"message": "La API del Bot de Gastos de Telegram está en ejecución."}
|
||||
|
||||
@app.post("/webhook/telegram", tags=["Webhooks"])
|
||||
async def process_telegram_update(request: dict):
|
||||
"""
|
||||
This endpoint would receive updates directly from a Telegram webhook.
|
||||
It needs to be implemented to parse the Telegram Update object and
|
||||
convert it into our internal RawInput model.
|
||||
Este endpoint recibiría actualizaciones directamente de un webhook de Telegram.
|
||||
Necesita ser implementado para analizar el objeto Update de Telegram y
|
||||
convertirlo en nuestro modelo RawInput interno.
|
||||
"""
|
||||
logger.info(f"Received Telegram update: {request}")
|
||||
# TODO: Implement a parser for the Telegram Update object.
|
||||
# For now, this is a placeholder.
|
||||
return {"status": "received", "message": "Telegram webhook handler not fully implemented."}
|
||||
logger.info(f"Actualización de Telegram recibida: {request}")
|
||||
# TODO: Implementar un analizador para el objeto Update de Telegram.
|
||||
# Por ahora, esto es un marcador de posición.
|
||||
return {"status": "received", "message": "El manejador de webhook de Telegram no está completamente implementado."}
|
||||
|
||||
@app.post("/process-expense", tags=["Processing"])
|
||||
@app.post("/process-expense", tags=["Procesamiento"])
|
||||
async def process_expense(raw_input: RawInput, db_session: Session = Depends(db.get_db)):
|
||||
"""
|
||||
Receives raw expense data, processes it through the full pipeline,
|
||||
and returns the result.
|
||||
Recibe datos de gastos sin procesar, los procesa a través del pipeline completo
|
||||
y devuelve el resultado.
|
||||
"""
|
||||
logger.info(f"Received raw input for processing: {raw_input.dict()}")
|
||||
logger.info(f"Entrada sin procesar recibida para procesamiento: {raw_input.dict()}")
|
||||
|
||||
try:
|
||||
result = process_expense_input(db=db_session, raw_input=raw_input)
|
||||
@@ -67,18 +67,18 @@ async def process_expense(raw_input: RawInput, db_session: Session = Depends(db.
|
||||
if result:
|
||||
return {"status": "success", "expense_id": result.id}
|
||||
else:
|
||||
# This could happen if confidence is low or an error occurred
|
||||
# Esto podría suceder si la confianza es baja o ocurrió un error
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Failed to process expense. It may require manual review or had invalid data."
|
||||
detail="Error al procesar el gasto. Puede requerir revisión manual o tenía datos inválidos."
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Validation error: {e}")
|
||||
logger.error(f"Error de validación: {e}")
|
||||
raise HTTPException(status_code=422, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.critical(f"An unexpected error occurred in the processing pipeline: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="An internal server error occurred.")
|
||||
logger.critical(f"Ocurrió un error inesperado en el pipeline de procesamiento: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Ocurrió un error interno del servidor.")
|
||||
|
||||
# To run this app:
|
||||
# Para ejecutar esta aplicación:
|
||||
# uvicorn app.main:app --reload
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""
|
||||
Handler for the /start command.
|
||||
Manejador para el comando /start.
|
||||
"""
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""Sends a welcome message when the /start command is issued."""
|
||||
"""Envía un mensaje de bienvenida cuando se emite el comando /start."""
|
||||
user = update.effective_user
|
||||
await update.message.reply_html(
|
||||
rf"Hi {user.mention_html()}! Welcome to the Expense Bot. "
|
||||
"Send me a message with an expense (e.g., 'lunch 25 eur') "
|
||||
"or forward a voice message or receipt image.",
|
||||
rf"¡Hola {user.mention_html()}! Bienvenido al Bot de Gastos. "
|
||||
"Envíame un mensaje con un gasto (ej., 'comida 25 eur') "
|
||||
"o reenvía un mensaje de voz o una imagen de un recibo.",
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""
|
||||
Handler for receiving and processing user messages (text, audio, images).
|
||||
Manejador para recibir y procesar mensajes de usuario (texto, audio, imágenes).
|
||||
"""
|
||||
from telegram import Update
|
||||
from telegram.ext import ContextTypes
|
||||
import logging
|
||||
|
||||
from app.schema.base import RawInput
|
||||
# This is a simplified integration. In a real app, you would likely
|
||||
# have a queue or a more robust way to trigger the processing pipeline.
|
||||
# Esta es una integración simplificada. En una aplicación real, probablemente
|
||||
# tendrías una cola o una forma más robusta de activar el pipeline de procesamiento.
|
||||
from app.router import process_expense_input
|
||||
from app.persistence.db import get_db
|
||||
|
||||
@@ -15,12 +15,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
|
||||
"""
|
||||
Handles regular messages and triggers the expense processing pipeline.
|
||||
Maneja mensajes regulares y activa el pipeline de procesamiento de gastos.
|
||||
"""
|
||||
user_id = str(update.effective_user.id)
|
||||
|
||||
# This is a very simplified example.
|
||||
# A real implementation needs to handle files, voice, etc.
|
||||
# Este es un ejemplo muy simplificado.
|
||||
# Una implementación real necesita manejar archivos, voz, etc.
|
||||
if update.message.text:
|
||||
raw_input = RawInput(
|
||||
user_id=user_id,
|
||||
@@ -29,20 +29,20 @@ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
)
|
||||
|
||||
try:
|
||||
# Get a DB session
|
||||
# Obtener una sesión de BD
|
||||
db_session = next(get_db())
|
||||
|
||||
# Run the processing pipeline
|
||||
# Ejecutar el pipeline de procesamiento
|
||||
result = process_expense_input(db=db_session, raw_input=raw_input)
|
||||
|
||||
if result:
|
||||
await update.message.reply_text(f"Expense saved successfully! ID: {result.id}")
|
||||
await update.message.reply_text(f"¡Gasto guardado con éxito! ID: {result.id}")
|
||||
else:
|
||||
await update.message.reply_text("I couldn't fully process that. It might need manual review.")
|
||||
await update.message.reply_text("No pude procesar eso completamente. Podría necesitar revisión manual.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling message: {e}", exc_info=True)
|
||||
await update.message.reply_text("Sorry, an error occurred while processing your request.")
|
||||
logger.error(f"Error al manejar el mensaje: {e}", exc_info=True)
|
||||
await update.message.reply_text("Lo siento, ocurrió un error al procesar tu solicitud.")
|
||||
|
||||
else:
|
||||
await update.message.reply_text("I can currently only process text messages.")
|
||||
await update.message.reply_text("Actualmente solo puedo procesar mensajes de texto.")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Database connection and session management.
|
||||
Conexión a la base de datos y gestión de sesiones.
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
@@ -11,7 +11,7 @@ from app.config import config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# The 'check_same_thread' argument is specific to SQLite.
|
||||
# El argumento 'check_same_thread' es específico de SQLite.
|
||||
engine_args = {"check_same_thread": False} if config.DATABASE_URL.startswith("sqlite") else {}
|
||||
|
||||
engine = create_engine(
|
||||
@@ -23,18 +23,18 @@ try:
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
logger.info("Database engine created successfully.")
|
||||
logger.info("Motor de base de datos creado con éxito.")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Failed to connect to the database: {e}")
|
||||
# Exit or handle the critical error appropriately
|
||||
logger.critical(f"Error al conectar con la base de datos: {e}")
|
||||
# Salir o manejar el error crítico apropiadamente
|
||||
engine = None
|
||||
SessionLocal = None
|
||||
Base = None
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
Dependency for FastAPI routes to get a DB session.
|
||||
Dependencia para que las rutas de FastAPI obtengan una sesión de BD.
|
||||
"""
|
||||
if SessionLocal is None:
|
||||
raise Exception("Database is not configured. Cannot create session.")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Data access layer for persistence.
|
||||
Contains functions to interact with the database.
|
||||
Capa de acceso a datos para la persistencia.
|
||||
Contiene funciones para interactuar con la base de datos.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Float, Date, DateTime, Text
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -11,7 +11,7 @@ from app.schema.base import FinalExpense
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Database ORM Model ---
|
||||
# --- Modelo ORM de Base de Datos ---
|
||||
class ExpenseDB(Base):
|
||||
__tablename__ = "expenses"
|
||||
|
||||
@@ -33,28 +33,28 @@ class ExpenseDB(Base):
|
||||
|
||||
def create_tables():
|
||||
"""
|
||||
Creates all database tables defined by models inheriting from Base.
|
||||
Crea todas las tablas de la base de datos definidas por los modelos que heredan de Base.
|
||||
"""
|
||||
if engine:
|
||||
logger.info("Creating database tables if they don't exist...")
|
||||
logger.info("Creando tablas de base de datos si no existen...")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
logger.info("Tables created successfully.")
|
||||
logger.info("Tablas creadas con éxito.")
|
||||
else:
|
||||
logger.error("Cannot create tables, database engine is not available.")
|
||||
logger.error("No se pueden crear las tablas, el motor de base de datos no está disponible.")
|
||||
|
||||
# --- Repository Functions ---
|
||||
# --- Funciones del Repositorio ---
|
||||
def save_final_expense(db: Session, expense: FinalExpense) -> ExpenseDB:
|
||||
"""
|
||||
Saves a user-confirmed expense to the database.
|
||||
Guarda un gasto confirmado por el usuario en la base de datos.
|
||||
|
||||
Args:
|
||||
db: The database session.
|
||||
expense: The FinalExpense object to save.
|
||||
db: La sesión de la base de datos.
|
||||
expense: El objeto FinalExpense a guardar.
|
||||
|
||||
Returns:
|
||||
The created ExpenseDB object.
|
||||
El objeto ExpenseDB creado.
|
||||
"""
|
||||
logger.info(f"Saving final expense for user {expense.user_id} to the database.")
|
||||
logger.info(f"Guardando gasto final para el usuario {expense.user_id} en la base de datos.")
|
||||
|
||||
db_expense = ExpenseDB(**expense.dict())
|
||||
|
||||
@@ -62,5 +62,5 @@ def save_final_expense(db: Session, expense: FinalExpense) -> ExpenseDB:
|
||||
db.commit()
|
||||
db.refresh(db_expense)
|
||||
|
||||
logger.info(f"Successfully saved expense with ID {db_expense.id}.")
|
||||
logger.info(f"Gasto guardado con éxito con ID {db_expense.id}.")
|
||||
return db_expense
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Configuration loader for providers and keywords.
|
||||
Cargador de configuración para proveedores y palabras clave.
|
||||
"""
|
||||
import csv
|
||||
import os
|
||||
@@ -8,43 +8,43 @@ from typing import List, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Paths to configuration files
|
||||
# Rutas a los archivos de configuración
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
PROVIDERS_PATH = os.path.join(BASE_DIR, 'config', 'providers.csv')
|
||||
KEYWORDS_PATH = os.path.join(BASE_DIR, 'config', 'keywords.csv')
|
||||
|
||||
def load_providers() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Loads the providers configuration from CSV.
|
||||
Carga la configuración de proveedores desde el archivo CSV.
|
||||
"""
|
||||
providers = []
|
||||
if not os.path.exists(PROVIDERS_PATH):
|
||||
logger.warning(f"Providers file not found at {PROVIDERS_PATH}")
|
||||
logger.warning(f"Archivo de proveedores no encontrado en {PROVIDERS_PATH}")
|
||||
return providers
|
||||
|
||||
try:
|
||||
with open(PROVIDERS_PATH, mode='r', encoding='utf-8') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
# Process aliases into a list
|
||||
# Procesar alias en una lista
|
||||
if 'aliases' in row and row['aliases']:
|
||||
row['aliases'] = [a.strip().lower() for a in row['aliases'].split(',')]
|
||||
else:
|
||||
row['aliases'] = []
|
||||
providers.append(row)
|
||||
logger.info(f"Loaded {len(providers)} providers from {PROVIDERS_PATH}")
|
||||
logger.info(f"Se cargaron {len(providers)} proveedores desde {PROVIDERS_PATH}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading providers: {e}")
|
||||
logger.error(f"Error al cargar proveedores: {e}")
|
||||
|
||||
return providers
|
||||
|
||||
def load_keywords() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Loads the keywords configuration from CSV.
|
||||
Carga la configuración de palabras clave desde el archivo CSV.
|
||||
"""
|
||||
keywords = []
|
||||
if not os.path.exists(KEYWORDS_PATH):
|
||||
logger.warning(f"Keywords file not found at {KEYWORDS_PATH}")
|
||||
logger.warning(f"Archivo de palabras clave no encontrado en {KEYWORDS_PATH}")
|
||||
return keywords
|
||||
|
||||
try:
|
||||
@@ -54,8 +54,8 @@ def load_keywords() -> List[Dict[str, Any]]:
|
||||
if 'keyword' in row:
|
||||
row['keyword'] = row['keyword'].strip().lower()
|
||||
keywords.append(row)
|
||||
logger.info(f"Loaded {len(keywords)} keywords from {KEYWORDS_PATH}")
|
||||
logger.info(f"Se cargaron {len(keywords)} palabras clave desde {KEYWORDS_PATH}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading keywords: {e}")
|
||||
logger.error(f"Error al cargar palabras clave: {e}")
|
||||
|
||||
return keywords
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Matching logic for providers and keywords.
|
||||
Lógica de coincidencia para proveedores y palabras clave.
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
@@ -7,13 +7,13 @@ from app.preprocessing.config_loader import load_providers, load_keywords
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global cache for configuration
|
||||
# Caché global para la configuración
|
||||
_PROVIDERS = None
|
||||
_KEYWORDS = None
|
||||
|
||||
def get_config():
|
||||
"""
|
||||
Returns the loaded configuration, using cache if available.
|
||||
Devuelve la configuración cargada, utilizando la caché si está disponible.
|
||||
"""
|
||||
global _PROVIDERS, _KEYWORDS
|
||||
if _PROVIDERS is None:
|
||||
@@ -24,7 +24,7 @@ def get_config():
|
||||
|
||||
def match_provider(description: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Searches for a provider name or alias in the description.
|
||||
Busca un nombre de proveedor o alias en la descripción.
|
||||
"""
|
||||
providers, _ = get_config()
|
||||
desc_lower = description.lower()
|
||||
@@ -33,11 +33,11 @@ def match_provider(description: str) -> Optional[Dict[str, Any]]:
|
||||
name = p.get('provider_name', '').lower()
|
||||
aliases = p.get('aliases', [])
|
||||
|
||||
# Check name
|
||||
# Verificar nombre
|
||||
if name and name in desc_lower:
|
||||
return p
|
||||
|
||||
# Check aliases
|
||||
# Verificar alias
|
||||
for alias in aliases:
|
||||
if alias and alias in desc_lower:
|
||||
return p
|
||||
@@ -46,7 +46,7 @@ def match_provider(description: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
def match_keywords(description: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Searches for keywords in the description.
|
||||
Busca palabras clave en la descripción.
|
||||
"""
|
||||
_, keywords = get_config()
|
||||
desc_lower = description.lower()
|
||||
@@ -60,13 +60,13 @@ def match_keywords(description: str) -> Optional[Dict[str, Any]]:
|
||||
|
||||
def get_metadata_from_match(description: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Attempts to find metadata (category, subcategory, etc.) for a description.
|
||||
Priority: Provider Match > Keyword Match.
|
||||
Intenta encontrar metadatos (categoría, subcategoría, etc.) para una descripción.
|
||||
Prioridad: Coincidencia de Proveedor > Coincidencia de Palabra Clave.
|
||||
"""
|
||||
# 1. Try Provider Match
|
||||
# 1. Intentar coincidencia de proveedor
|
||||
provider = match_provider(description)
|
||||
if provider:
|
||||
logger.info(f"Matched provider: {provider['provider_name']}")
|
||||
logger.info(f"Proveedor coincidente: {provider['provider_name']}")
|
||||
return {
|
||||
"category": provider.get('categoria_principal'),
|
||||
"subcategory": provider.get('subcategoria'),
|
||||
@@ -75,10 +75,10 @@ def get_metadata_from_match(description: str) -> Dict[str, Any]:
|
||||
"matched_name": provider['provider_name']
|
||||
}
|
||||
|
||||
# 2. Try Keyword Match
|
||||
# 2. Intentar coincidencia de palabra clave
|
||||
keyword = match_keywords(description)
|
||||
if keyword:
|
||||
logger.info(f"Matched keyword: {keyword['keyword']}")
|
||||
logger.info(f"Palabra clave coincidente: {keyword['keyword']}")
|
||||
return {
|
||||
"category": keyword.get('categoria_principal'),
|
||||
"subcategory": keyword.get('subcategoria'),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Main application router.
|
||||
Enrutador principal de la aplicación.
|
||||
|
||||
Orchestrates the entire expense processing workflow, from input to persistence.
|
||||
Orquesta todo el flujo de trabajo de procesamiento de gastos, desde la entrada hasta la persistencia.
|
||||
"""
|
||||
import logging
|
||||
|
||||
@@ -16,55 +16,55 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def process_expense_input(db: Session, raw_input: RawInput) -> FinalExpense:
|
||||
"""
|
||||
Full pipeline for processing a raw input.
|
||||
Pipeline completo para procesar una entrada sin procesar.
|
||||
|
||||
1. Ingestion: Convert input (text, image, etc.) to raw text.
|
||||
2. AI Extraction: Parse the raw text into structured data.
|
||||
3. AI Classification/Audit: Validate and categorize the expense.
|
||||
4. Persistence: Save the final, confirmed expense to the database.
|
||||
1. Ingestión: Convertir la entrada (texto, imagen, etc.) en texto sin procesar.
|
||||
2. Extracción por IA: Analizar el texto sin procesar en datos estructurados.
|
||||
3. Clasificación/Auditoría por IA: Validar y categorizar el gasto.
|
||||
4. Persistencia: Guardar el gasto final confirmado en la base de datos.
|
||||
"""
|
||||
logger.info(f"Router processing input for user {raw_input.user_id} of type {raw_input.input_type}")
|
||||
logger.info(f"El enrutador está procesando la entrada para el usuario {raw_input.user_id} de tipo {raw_input.input_type}")
|
||||
|
||||
# 1. Ingestion
|
||||
# 1. Ingestión
|
||||
raw_text = ""
|
||||
if raw_input.input_type == "text":
|
||||
raw_text = text.process_text_input(raw_input.data)
|
||||
elif raw_input.input_type == "image":
|
||||
# In a real app, data would be bytes, not a string path
|
||||
# En una aplicación real, los datos serían bytes, no una ruta de cadena
|
||||
raw_text = image.process_image_input(raw_input.data.encode())
|
||||
elif raw_input.input_type == "audio":
|
||||
raw_text = audio.process_audio_input(raw_input.data.encode())
|
||||
elif raw_input.input_type == "document":
|
||||
raw_text = document.process_document_input(raw_input.data.encode())
|
||||
else:
|
||||
raise ValueError(f"Unsupported input type: {raw_input.input_type}")
|
||||
raise ValueError(f"Tipo de entrada no soportado: {raw_input.input_type}")
|
||||
|
||||
if not raw_text:
|
||||
logger.error("Ingestion phase resulted in empty text. Aborting.")
|
||||
# We might want to return a specific status here
|
||||
logger.error("La fase de ingestión resultó en un texto vacío. Abortando.")
|
||||
# Podríamos querer devolver un estado específico aquí
|
||||
return None
|
||||
|
||||
# 2. AI Extraction
|
||||
# 2. Extracción por IA
|
||||
extracted_data = extractor.extract_expense_data(raw_text)
|
||||
if not extracted_data.amount or not extracted_data.description:
|
||||
logger.error("AI extraction failed to find key details. Aborting.")
|
||||
logger.error("La extracción por IA no pudo encontrar detalles clave. Abortando.")
|
||||
return None
|
||||
|
||||
# 3. AI Classification & Confirmation (simplified)
|
||||
# In a real bot, you would present this to the user for confirmation.
|
||||
# 3. Clasificación y Confirmación por IA (simplificado)
|
||||
# En un bot real, presentarías esto al usuario para su confirmación.
|
||||
provisional_expense = ProvisionalExpense(
|
||||
user_id=raw_input.user_id,
|
||||
extracted_data=extracted_data,
|
||||
confidence_score=0.0 # Will be set by classifier
|
||||
confidence_score=0.0 # Será establecido por el clasificador
|
||||
)
|
||||
|
||||
audited_expense = classifier.classify_and_audit(provisional_expense)
|
||||
|
||||
# 3.5 Deterministic Matching (Phase 3)
|
||||
# Enrich data with categories from providers/keywords if available
|
||||
# 3.5 Coincidencia Determinística (Fase 3)
|
||||
# Enriquecer los datos con categorías de proveedores/palabras clave si están disponibles
|
||||
match_metadata = matcher.get_metadata_from_match(extracted_data.description)
|
||||
|
||||
# For now, we auto-confirm if confidence is high.
|
||||
# Por ahora, auto-confirmamos si la confianza es alta.
|
||||
if audited_expense.confidence_score > 0.7:
|
||||
final_expense = FinalExpense(
|
||||
user_id=audited_expense.user_id,
|
||||
@@ -79,12 +79,12 @@ def process_expense_input(db: Session, raw_input: RawInput) -> FinalExpense:
|
||||
confirmed_by="auto-confirm"
|
||||
)
|
||||
|
||||
# 4. Persistence
|
||||
# 4. Persistencia
|
||||
db_record = repositories.save_final_expense(db, final_expense)
|
||||
logger.info(f"Successfully processed and saved expense ID {db_record.id}")
|
||||
logger.info(f"Gasto procesado y guardado con éxito ID {db_record.id}")
|
||||
return db_record
|
||||
|
||||
else:
|
||||
logger.warning(f"Expense for user {raw_input.user_id} has low confidence. Awaiting manual confirmation.")
|
||||
# Here you would store the provisional expense and notify the user
|
||||
logger.warning(f"El gasto para el usuario {raw_input.user_id} tiene baja confianza. Esperando confirmación manual.")
|
||||
# Aquí guardarías el gasto provisional y notificarías al usuario
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user