feat: translate comments, docstrings, and log messages to Spanish.

This commit is contained in:
Marco Gallegos
2025-12-18 12:33:39 -06:00
parent 519a5ad705
commit 3b9346fe0f
12 changed files with 205 additions and 205 deletions

View File

@@ -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.")

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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.",
)

View File

@@ -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.")

View File

@@ -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.")

View File

@@ -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

View File

@@ -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

View File

@@ -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'),

View File

@@ -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