commit a5ed36ef96b08b8cd4c3038a2ea091416277edca Author: Juan M. Ley Date: Sat Apr 25 15:20:23 2026 -0600 first and real commit diff --git a/api/__pycache__/api.cpython-314.pyc b/api/__pycache__/api.cpython-314.pyc new file mode 100644 index 0000000..a7d8859 Binary files /dev/null and b/api/__pycache__/api.cpython-314.pyc differ diff --git a/api/__pycache__/router.cpython-314.pyc b/api/__pycache__/router.cpython-314.pyc new file mode 100644 index 0000000..a0ff10b Binary files /dev/null and b/api/__pycache__/router.cpython-314.pyc differ diff --git a/api/__pycache__/routes.cpython-314.pyc b/api/__pycache__/routes.cpython-314.pyc new file mode 100644 index 0000000..88daeb4 Binary files /dev/null and b/api/__pycache__/routes.cpython-314.pyc differ diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000..f4e9807 --- /dev/null +++ b/api/api.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from api.routes.root import router as rootRouter +from api.routes.ai import router as aiRouter + + +application = FastAPI() + +application.add_middleware(CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_headers=['*'], + allow_methods=['GET','POST']) + + +application.include_router(router=rootRouter, prefix='') +application.include_router(router=aiRouter, prefix='/ai') diff --git a/api/routes/__pycache__/ai.cpython-314.pyc b/api/routes/__pycache__/ai.cpython-314.pyc new file mode 100644 index 0000000..939419d Binary files /dev/null and b/api/routes/__pycache__/ai.cpython-314.pyc differ diff --git a/api/routes/__pycache__/root.cpython-314.pyc b/api/routes/__pycache__/root.cpython-314.pyc new file mode 100644 index 0000000..3edc537 Binary files /dev/null and b/api/routes/__pycache__/root.cpython-314.pyc differ diff --git a/api/routes/ai.py b/api/routes/ai.py new file mode 100644 index 0000000..17dd70f --- /dev/null +++ b/api/routes/ai.py @@ -0,0 +1,89 @@ +import anthropic, json +from fastapi import APIRouter, HTTPException +from fastapi.responses import StreamingResponse +from dotenv import load_dotenv +from os import getenv + +router = APIRouter() + +load_dotenv(".env_file") +client = anthropic.Anthropic(api_key=getenv('CLAUDE_KEY')) + +with open('/home/rodo/Documents/py/LittleAPI/list.json', 'r') as f: + config = json.load(f) + pdf_file_ids = config.get("files", []) + +@router.post("/using_docs", tags=["AI"]) +async def send_message_using_docs(message:str): + """ + Envía un mensaje usando documentos PDF almacenados. + + - **message**: El mensaje de texto a procesar + - **Retorna**: Stream de texto con la respuesta de Claude + """ + try: + content = [ + { + "type": "text", + "text": message + } + ] + + for file_id in pdf_file_ids: + content.append( + { + "type": "document", + "source" : { + "type" : "file", + "file_id": file_id + } + } + ) + + def event_generator(): + with client.beta.messages.stream( + model="claude-haiku-4-5", + max_tokens=2048, + messages=[{ + "role": "user", + "content": content + }], + betas=["files-api-2025-04-14"], + ) as stream: + for text in stream.text_stream: + yield text + return StreamingResponse(event_generator(), media_type="text/plain") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/", tags=["AI"]) +async def send_message(message:str): + """ + Envía un mensaje normal a Claude. + + - **message**: El mensaje de texto a procesar + - **Retorna**: Stream de texto con la respuesta de Claude + """ + try: + content = [ + { + "type": "text", + "text": message + } + ] + + def event_generator(): + with client.beta.messages.stream( + model="claude-haiku-4-5", + max_tokens=512, + messages=[{ + "role": "user", + "content": content + }], + ) as stream: + for text in stream.text_stream: + yield text + + return StreamingResponse(event_generator(), media_type="text/plain") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/api/routes/root.py b/api/routes/root.py new file mode 100644 index 0000000..45c98c5 --- /dev/null +++ b/api/routes/root.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get('/', tags=["root"]) +def root(): + return { + "status": "running" + } \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..ec71157 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,56 @@ +# LittleAPI Frontend + +Frontend independiente en PyQt6 para consumir la API del proyecto LittleAPI. + +## Instalación + +1. Navega a la carpeta frontend: +```bash +cd frontend +``` + +2. Crea un entorno virtual (opcional pero recomendado): +```bash +python -m venv venv +source venv/bin/activate # En Windows: venv\Scripts\activate +``` + +3. Instala las dependencias: +```bash +pip install -r requirements.txt +``` + +## Uso + +1. Asegúrate de que el backend esté corriendo en `http://localhost:9900`: +```bash +python main.py # En la carpeta raíz del proyecto +``` + +2. En otra terminal, ejecuta el frontend desde la carpeta `frontend`: +```bash +python main.py +``` + +## Características + +- ✓ Verificación del estado de la API +- ✓ Envío de mensajes normales +- ✓ Envío de mensajes usando documentos +- ✓ Respuestas en streaming +- ✓ Interfaz simple y limpia con PyQt6 +- ✓ Manejo de errores de conexión + +## Estructura + +- `main.py` - Aplicación principal con interfaz PyQt6 +- `config.py` - Configuración (URLs, dimensiones, etc.) +- `requirements.txt` - Dependencias del proyecto + +## Configuración + +Edita `config.py` para cambiar: +- URL de la API +- Dimensiones de la ventana +- Timeouts +- Otros parámetros diff --git a/frontend/__pycache__/config.cpython-314.pyc b/frontend/__pycache__/config.cpython-314.pyc new file mode 100644 index 0000000..d0166e6 Binary files /dev/null and b/frontend/__pycache__/config.cpython-314.pyc differ diff --git a/frontend/config.py b/frontend/config.py new file mode 100644 index 0000000..20e0cdd --- /dev/null +++ b/frontend/config.py @@ -0,0 +1,18 @@ +from dotenv import load_dotenv +from os import getenv + +load_dotenv('.env_file') + +# Configuración de la aplicación frontend +API_BASE_URL = f"http://{getenv('HOST')}:{getenv('PORT')}" +API_HEALTH_ENDPOINT = f"{API_BASE_URL}/" +API_MESSAGE_ENDPOINT = f"{API_BASE_URL}/ai/" +API_MESSAGE_DOCS_ENDPOINT = f"{API_BASE_URL}/ai/using_docs" + +# Configuración de la ventana +WINDOW_WIDTH = 900 +WINDOW_HEIGHT = 700 +WINDOW_TITLE = "LittleAPI Frontend" + +# Configuración de timeout para requests +REQUEST_TIMEOUT = 30 diff --git a/frontend/main.py b/frontend/main.py new file mode 100644 index 0000000..32b7ac6 --- /dev/null +++ b/frontend/main.py @@ -0,0 +1,349 @@ +import sys +import requests +import markdown +from PyQt6.QtWidgets import ( + QApplication, + QMainWindow, + QWidget, + QVBoxLayout, + QHBoxLayout, + QTextEdit, + QTextBrowser, + QPushButton, + QLabel, + QRadioButton, + QButtonGroup, + QMessageBox, +) +from PyQt6.QtCore import Qt, QThread, pyqtSignal +from PyQt6.QtGui import QFont +from config import ( + API_BASE_URL, + API_HEALTH_ENDPOINT, + API_MESSAGE_ENDPOINT, + API_MESSAGE_DOCS_ENDPOINT, + WINDOW_WIDTH, + WINDOW_HEIGHT, + WINDOW_TITLE, + REQUEST_TIMEOUT, +) + + +def markdown_to_html(text): + """Convierte markdown a HTML con soporte para tablas, títulos, negritas y cursivas""" + html = markdown.markdown( + text, + extensions=['tables', 'sane_lists', 'extra'] + ) + # Agregar CSS para mejorar la presentación + styled_html = f""" + + + + + + {html} + + + """ + return styled_html + + +class APIWorker(QThread): + """Thread worker para hacer requests a la API sin bloquear la UI""" + response_received = pyqtSignal(str) + error_occurred = pyqtSignal(str) + finished = pyqtSignal() + + def __init__(self, endpoint, message, use_streaming=True): + super().__init__() + self.endpoint = endpoint + self.message = message + self.use_streaming = use_streaming + + def run(self): + try: + if self.use_streaming: + self._handle_streaming_response() + else: + self._handle_normal_response() + except requests.exceptions.ConnectionError: + self.error_occurred.emit( + "Error de conexión: No se puede conectar a la API.\n" + f"Asegúrate de que el servidor esté corriendo en {API_BASE_URL}" + ) + except requests.exceptions.Timeout: + self.error_occurred.emit("Error de timeout: La API tardó demasiado en responder.") + except Exception as e: + self.error_occurred.emit(f"Error: {str(e)}") + finally: + self.finished.emit() + + def _handle_streaming_response(self): + """Maneja respuestas en streaming""" + try: + response = requests.post( + self.endpoint, + params={"message": self.message}, + stream=True, + timeout=REQUEST_TIMEOUT, + ) + response.raise_for_status() + + for chunk in response.iter_content(decode_unicode=True, chunk_size=10): + if chunk: + self.response_received.emit(chunk) + except Exception as e: + raise e + + def _handle_normal_response(self): + """Maneja respuestas normales""" + response = requests.post( + self.endpoint, + params={"message": self.message}, + timeout=REQUEST_TIMEOUT, + ) + response.raise_for_status() + self.response_received.emit(response.text) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle(WINDOW_TITLE) + self.setGeometry(100, 100, WINDOW_WIDTH, WINDOW_HEIGHT) + self.worker = None + self.api_status = False + self.response_buffer = "" # Buffer para acumular la respuesta + + self.initUI() + self.check_api_status() + + def initUI(self): + """Inicializa la interfaz de usuario""" + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QVBoxLayout() + + # Sección de estado + status_layout = QHBoxLayout() + self.status_label = QLabel("● Estado: Verificando...") + self.status_label.setFont(QFont("Arial", 10)) + status_layout.addWidget(self.status_label) + status_layout.addStretch() + refresh_btn = QPushButton("Actualizar estado") + refresh_btn.clicked.connect(self.check_api_status) + status_layout.addWidget(refresh_btn) + main_layout.addLayout(status_layout) + + # Selector de modo + mode_layout = QHBoxLayout() + mode_label = QLabel("Modo:") + mode_layout.addWidget(mode_label) + + self.mode_group = QButtonGroup() + self.radio_normal = QRadioButton("Mensaje normal") + self.radio_docs = QRadioButton("Con documentos") + self.radio_normal.setChecked(True) + self.mode_group.addButton(self.radio_normal) + self.mode_group.addButton(self.radio_docs) + mode_layout.addWidget(self.radio_normal) + mode_layout.addWidget(self.radio_docs) + mode_layout.addStretch() + main_layout.addLayout(mode_layout) + + # Área de entrada + input_label = QLabel("Mensaje:") + main_layout.addWidget(input_label) + self.input_text = QTextEdit() + self.input_text.setPlaceholderText("Escribe tu mensaje aquí...") + self.input_text.setMaximumHeight(120) + main_layout.addWidget(self.input_text) + + # Botones de acción + button_layout = QHBoxLayout() + self.send_btn = QPushButton("Enviar") + self.send_btn.clicked.connect(self.send_message) + self.clear_btn = QPushButton("Limpiar") + self.clear_btn.clicked.connect(self.clear_all) + button_layout.addWidget(self.send_btn) + button_layout.addWidget(self.clear_btn) + button_layout.addStretch() + main_layout.addLayout(button_layout) + + # Área de respuesta + response_label = QLabel("Respuesta:") + main_layout.addWidget(response_label) + self.response_text = QTextBrowser() + self.response_text.setMarkdown("La respuesta de la API aparecerá aquí...") + main_layout.addWidget(self.response_text) + + central_widget.setLayout(main_layout) + + def check_api_status(self): + """Verifica el estado del API""" + try: + response = requests.get(API_HEALTH_ENDPOINT, timeout=REQUEST_TIMEOUT) + if response.status_code == 200: + self.api_status = True + self.status_label.setText("● Estado: API conectada ✓") + self.status_label.setStyleSheet("color: green;") + return + except Exception: + pass + + self.api_status = False + self.status_label.setText("● Estado: API desconectada ✗") + self.status_label.setStyleSheet("color: red;") + + def send_message(self): + """Envía el mensaje a la API""" + if not self.api_status: + QMessageBox.warning(self, "Error", "La API no está conectada.") + return + + message = self.input_text.toPlainText().strip() + if not message: + QMessageBox.warning(self, "Advertencia", "Por favor escribe un mensaje.") + return + + # Determinar endpoint + use_docs = self.radio_docs.isChecked() + endpoint = API_MESSAGE_DOCS_ENDPOINT if use_docs else API_MESSAGE_ENDPOINT + + # Limpiar respuesta anterior + self.response_buffer = "" + self.response_text.clear() + + # Deshabilitar botón de envío + self.send_btn.setEnabled(False) + self.send_btn.setText("Procesando...") + + # Crear y ejecutar worker + self.worker = APIWorker(endpoint, message, use_streaming=True) + self.worker.response_received.connect(self.append_response) + self.worker.error_occurred.connect(self.handle_error) + self.worker.finished.connect(self.on_request_finished) + self.worker.start() + + def append_response(self, chunk): + """Acumula chunks de respuesta y los renderiza como markdown""" + self.response_buffer += chunk + # Renderizar el markdown con HTML + html = markdown_to_html(self.response_buffer) + self.response_text.setHtml(html) + # Auto-scroll al final + scrollbar = self.response_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def handle_error(self, error_message): + """Maneja errores""" + error_md = f"**Error:** {error_message}" + html = markdown_to_html(error_md) + self.response_text.setHtml(html) + QMessageBox.critical(self, "Error", error_message) + + def on_request_finished(self): + """Se ejecuta cuando la request termina""" + self.send_btn.setEnabled(True) + self.send_btn.setText("Enviar") + self.check_api_status() + + def clear_all(self): + """Limpia el mensaje y la respuesta""" + self.input_text.clear() + self.response_buffer = "" + self.response_text.clear() + self.input_text.setFocus() + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/frontend/requirements.txt b/frontend/requirements.txt new file mode 100644 index 0000000..021ec45 --- /dev/null +++ b/frontend/requirements.txt @@ -0,0 +1,5 @@ +PyQt6==6.7.1 +PyQt6-Qt6==6.7.1 +PyQt6-sip==13.6.1 +requests==2.32.3 +markdown==3.5.2 diff --git a/html_client.html b/html_client.html new file mode 100644 index 0000000..493a780 --- /dev/null +++ b/html_client.html @@ -0,0 +1,423 @@ + + + + + + Claude API Chat + + + +
+
+

🤖 Claude API Chat

+

Interfaz de chat con tu API de FastAPI

+
+ + +
+
+ +
+ + +
+ +
+
+
+ ¡Hola! 👋 Estoy listo para ayudarte. Puedes cambiar entre modo normal y modo con documentos usando los botones superiores. +
+
+
+ +
+
+ + + +
+
+ + +
+
+
+ + + + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..277bc24 --- /dev/null +++ b/main.py @@ -0,0 +1,12 @@ +import uvicorn +from os import getenv; from dotenv import load_dotenv + +load_dotenv('.env_file') + +if __name__ == "__main__": + uvicorn.run( + app="api.api:application", + port=int(getenv('PORT')), + host=getenv('HOST'), + reload=True + ) \ No newline at end of file