bentosmau commited on
Commit ·
bc2ac28
1
Parent(s): fc132b8
Implement API key authentication for enhanced NeoAPI security
Browse filesAdd API key generation, validation, and revocation endpoints to the API server, along with a JavaScript client for frontend integration.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e3ff2484-bbd8-4aba-bea0-1940769b874a
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b75b4233-01b4-45d5-b7c0-6e836674316e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1739408b-93a5-479b-a658-30f2493b0467/e3ff2484-bbd8-4aba-bea0-1940769b874a/eHvZpi8
Replit-Helium-Checkpoint-Created: true
- artifacts/api-server/data/api-keys.json +10 -0
- artifacts/api-server/src/lib/apiKeys.ts +84 -0
- artifacts/api-server/src/middlewares/auth.ts +32 -0
- artifacts/api-server/src/routes/index.ts +5 -1
- artifacts/api-server/src/routes/keys.ts +36 -0
- artifacts/api-server/src/routes/neo.ts +58 -0
- chat-app/app.py +11 -147
- chat-app/logica.py +176 -0
- chat-app/neo_rest.py +62 -0
- neo-api-client.js +149 -0
artifacts/api-server/data/api-keys.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"key": "neo1-0ebbf10959c3ac6d2023d9e9f3111f0051c91a0d",
|
| 4 |
+
"name": "mi-pagina-web",
|
| 5 |
+
"createdAt": "2026-04-14T21:31:10.658Z",
|
| 6 |
+
"active": true,
|
| 7 |
+
"usageCount": 1,
|
| 8 |
+
"lastUsed": "2026-04-14T21:31:15.634Z"
|
| 9 |
+
}
|
| 10 |
+
]
|
artifacts/api-server/src/lib/apiKeys.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from "fs";
|
| 2 |
+
import path from "path";
|
| 3 |
+
import crypto from "crypto";
|
| 4 |
+
|
| 5 |
+
const DATA_DIR = path.join(process.cwd(), "data");
|
| 6 |
+
const KEYS_FILE = path.join(DATA_DIR, "api-keys.json");
|
| 7 |
+
|
| 8 |
+
export interface ApiKey {
|
| 9 |
+
key: string;
|
| 10 |
+
name: string;
|
| 11 |
+
createdAt: string;
|
| 12 |
+
active: boolean;
|
| 13 |
+
usageCount: number;
|
| 14 |
+
lastUsed: string | null;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
function ensureDataDir() {
|
| 18 |
+
if (!fs.existsSync(DATA_DIR)) {
|
| 19 |
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function readKeys(): ApiKey[] {
|
| 24 |
+
ensureDataDir();
|
| 25 |
+
if (!fs.existsSync(KEYS_FILE)) return [];
|
| 26 |
+
try {
|
| 27 |
+
return JSON.parse(fs.readFileSync(KEYS_FILE, "utf-8"));
|
| 28 |
+
} catch {
|
| 29 |
+
return [];
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function writeKeys(keys: ApiKey[]) {
|
| 34 |
+
ensureDataDir();
|
| 35 |
+
fs.writeFileSync(KEYS_FILE, JSON.stringify(keys, null, 2), "utf-8");
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function generateKey(name: string): ApiKey {
|
| 39 |
+
const keys = readKeys();
|
| 40 |
+
const newKey: ApiKey = {
|
| 41 |
+
key: `neo1-${crypto.randomBytes(20).toString("hex")}`,
|
| 42 |
+
name,
|
| 43 |
+
createdAt: new Date().toISOString(),
|
| 44 |
+
active: true,
|
| 45 |
+
usageCount: 0,
|
| 46 |
+
lastUsed: null,
|
| 47 |
+
};
|
| 48 |
+
keys.push(newKey);
|
| 49 |
+
writeKeys(keys);
|
| 50 |
+
return newKey;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export function listKeys(): Omit<ApiKey, "key">[] {
|
| 54 |
+
return readKeys().map(({ key, ...rest }) => ({
|
| 55 |
+
...rest,
|
| 56 |
+
keyPreview: `${key.slice(0, 10)}...${key.slice(-4)}`,
|
| 57 |
+
})) as Omit<ApiKey, "key">[];
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export function validateKey(key: string): ApiKey | null {
|
| 61 |
+
const keys = readKeys();
|
| 62 |
+
const found = keys.find((k) => k.key === key && k.active);
|
| 63 |
+
if (!found) return null;
|
| 64 |
+
|
| 65 |
+
found.usageCount += 1;
|
| 66 |
+
found.lastUsed = new Date().toISOString();
|
| 67 |
+
writeKeys(keys);
|
| 68 |
+
return found;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export function revokeKey(keyOrName: string): boolean {
|
| 72 |
+
const keys = readKeys();
|
| 73 |
+
const idx = keys.findIndex(
|
| 74 |
+
(k) => k.key === keyOrName || k.name === keyOrName
|
| 75 |
+
);
|
| 76 |
+
if (idx === -1) return false;
|
| 77 |
+
keys[idx].active = false;
|
| 78 |
+
writeKeys(keys);
|
| 79 |
+
return true;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
export function getAdminSecret(): string {
|
| 83 |
+
return process.env["NEO_ADMIN_SECRET"] ?? "neo-admin-change-me";
|
| 84 |
+
}
|
artifacts/api-server/src/middlewares/auth.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Request, Response, NextFunction } from "express";
|
| 2 |
+
import { validateKey, getAdminSecret } from "../lib/apiKeys.js";
|
| 3 |
+
|
| 4 |
+
export function requireApiKey(req: Request, res: Response, next: NextFunction) {
|
| 5 |
+
const authHeader = req.headers["authorization"] ?? "";
|
| 6 |
+
const key = authHeader.startsWith("Bearer ")
|
| 7 |
+
? authHeader.slice(7).trim()
|
| 8 |
+
: (req.headers["x-api-key"] as string | undefined)?.trim() ?? "";
|
| 9 |
+
|
| 10 |
+
if (!key) {
|
| 11 |
+
res.status(401).json({ error: "API key requerida. Usa el header: Authorization: Bearer <tu-key>" });
|
| 12 |
+
return;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const found = validateKey(key);
|
| 16 |
+
if (!found) {
|
| 17 |
+
res.status(403).json({ error: "API key inválida o revocada." });
|
| 18 |
+
return;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
(req as any).apiKey = found;
|
| 22 |
+
next();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export function requireAdmin(req: Request, res: Response, next: NextFunction) {
|
| 26 |
+
const secret = req.headers["x-admin-secret"] as string | undefined;
|
| 27 |
+
if (!secret || secret !== getAdminSecret()) {
|
| 28 |
+
res.status(403).json({ error: "Admin secret incorrecto." });
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
next();
|
| 32 |
+
}
|
artifacts/api-server/src/routes/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
| 1 |
import { Router, type IRouter } from "express";
|
| 2 |
-
import healthRouter from "./health";
|
|
|
|
|
|
|
| 3 |
|
| 4 |
const router: IRouter = Router();
|
| 5 |
|
| 6 |
router.use(healthRouter);
|
|
|
|
|
|
|
| 7 |
|
| 8 |
export default router;
|
|
|
|
| 1 |
import { Router, type IRouter } from "express";
|
| 2 |
+
import healthRouter from "./health.js";
|
| 3 |
+
import keysRouter from "./keys.js";
|
| 4 |
+
import neoRouter from "./neo.js";
|
| 5 |
|
| 6 |
const router: IRouter = Router();
|
| 7 |
|
| 8 |
router.use(healthRouter);
|
| 9 |
+
router.use(keysRouter);
|
| 10 |
+
router.use(neoRouter);
|
| 11 |
|
| 12 |
export default router;
|
artifacts/api-server/src/routes/keys.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from "express";
|
| 2 |
+
import { requireAdmin } from "../middlewares/auth.js";
|
| 3 |
+
import { generateKey, listKeys, revokeKey } from "../lib/apiKeys.js";
|
| 4 |
+
|
| 5 |
+
const router = Router();
|
| 6 |
+
|
| 7 |
+
router.post("/keys", requireAdmin, (req, res) => {
|
| 8 |
+
const { name } = req.body as { name?: string };
|
| 9 |
+
if (!name || typeof name !== "string" || !name.trim()) {
|
| 10 |
+
res.status(400).json({ error: "El campo 'name' es obligatorio." });
|
| 11 |
+
return;
|
| 12 |
+
}
|
| 13 |
+
const key = generateKey(name.trim());
|
| 14 |
+
res.status(201).json({
|
| 15 |
+
message: "API key generada. Guárdala, no se vuelve a mostrar.",
|
| 16 |
+
key: key.key,
|
| 17 |
+
name: key.name,
|
| 18 |
+
createdAt: key.createdAt,
|
| 19 |
+
});
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
router.get("/keys", requireAdmin, (_req, res) => {
|
| 23 |
+
res.json({ keys: listKeys() });
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
router.delete("/keys/:name", requireAdmin, (req, res) => {
|
| 27 |
+
const { name } = req.params;
|
| 28 |
+
const ok = revokeKey(name!);
|
| 29 |
+
if (!ok) {
|
| 30 |
+
res.status(404).json({ error: "Key no encontrada o ya revocada." });
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
res.json({ message: `Key '${name}' revocada.` });
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
export default router;
|
artifacts/api-server/src/routes/neo.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Router } from "express";
|
| 2 |
+
import { requireApiKey } from "../middlewares/auth.js";
|
| 3 |
+
|
| 4 |
+
const router = Router();
|
| 5 |
+
const NEO_REST_URL = "http://localhost:5001";
|
| 6 |
+
|
| 7 |
+
router.post("/neo/chat", requireApiKey, async (req, res) => {
|
| 8 |
+
const { message, history = [] } = req.body as {
|
| 9 |
+
message?: string;
|
| 10 |
+
history?: Array<{ role: string; content: string }>;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
if (!message || typeof message !== "string" || !message.trim()) {
|
| 14 |
+
res.status(400).json({ error: "El campo 'message' es obligatorio." });
|
| 15 |
+
return;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
try {
|
| 19 |
+
const response = await fetch(`${NEO_REST_URL}/chat`, {
|
| 20 |
+
method: "POST",
|
| 21 |
+
headers: { "Content-Type": "application/json" },
|
| 22 |
+
body: JSON.stringify({ message: message.trim(), history }),
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
if (!response.ok) {
|
| 26 |
+
const err = await response.text();
|
| 27 |
+
res.status(502).json({ error: "Error del modelo NEO-1", detail: err });
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const data = await response.json() as { response: string; model: string; status: string };
|
| 32 |
+
res.json({
|
| 33 |
+
response: data.response,
|
| 34 |
+
model: data.model ?? "mdfjbots-neo-1",
|
| 35 |
+
status: "ok",
|
| 36 |
+
});
|
| 37 |
+
} catch (err: any) {
|
| 38 |
+
res.status(503).json({
|
| 39 |
+
error: "No se pudo conectar con el modelo NEO-1. ¿Está corriendo?",
|
| 40 |
+
detail: err?.message ?? String(err),
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
router.get("/neo/models", requireApiKey, (_req, res) => {
|
| 46 |
+
res.json({
|
| 47 |
+
models: [
|
| 48 |
+
{
|
| 49 |
+
id: "mdfjbots-neo-1",
|
| 50 |
+
name: "NEO-1",
|
| 51 |
+
description: "Asistente conversacional con soporte de Roblox, calculadora y temas educativos.",
|
| 52 |
+
version: "1.13.1",
|
| 53 |
+
},
|
| 54 |
+
],
|
| 55 |
+
});
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
export default router;
|
chat-app/app.py
CHANGED
|
@@ -1,142 +1,17 @@
|
|
| 1 |
import os
|
| 2 |
-
import re
|
| 3 |
-
import json
|
| 4 |
import time
|
| 5 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
|
| 7 |
from matematicas import (
|
| 8 |
es_solicitud_calculadora, es_operacion_matematica,
|
| 9 |
-
resolver_operacion, formatear_resultado, extraer_nombre_usuario
|
| 10 |
)
|
| 11 |
|
| 12 |
-
GENEROS_ROBLOX = {
|
| 13 |
-
"Town and City": "un juego de vida urbana y roleplay social",
|
| 14 |
-
"Adventure": "un juego de aventura y exploración",
|
| 15 |
-
"Role Playing": "un juego de rol donde puedes ser quien quieras",
|
| 16 |
-
"Comedy": "un juego de comedia y entretenimiento",
|
| 17 |
-
"Action": "un juego de acción y combate",
|
| 18 |
-
"Horror": "un juego de terror y suspenso",
|
| 19 |
-
"Military": "un juego de temática militar y estrategia",
|
| 20 |
-
"Medieval": "un juego de temática medieval con castillos y caballeros",
|
| 21 |
-
"Naval": "un juego de aventuras navales y piratas",
|
| 22 |
-
"Sci-Fi": "un juego de ciencia ficción",
|
| 23 |
-
"Sports": "un juego deportivo",
|
| 24 |
-
"Fighting": "un juego de peleas y combate",
|
| 25 |
-
"Western": "un juego de temática del lejano oeste",
|
| 26 |
-
"FPS": "un juego de disparos en primera persona",
|
| 27 |
-
"Building": "un juego de construcción y creatividad",
|
| 28 |
-
"Simulator": "un simulador",
|
| 29 |
-
"Tycoon": "un juego de gestión y negocios (tycoon)",
|
| 30 |
-
"Obby": "un juego de obstáculos (obby)",
|
| 31 |
-
"All Genres": "un juego variado",
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
def cargar_respuestas():
|
| 35 |
-
ruta = os.path.join(os.path.dirname(__file__), "respuestas.json")
|
| 36 |
-
try:
|
| 37 |
-
with open(ruta, "r", encoding="utf-8") as f:
|
| 38 |
-
datos = json.load(f)
|
| 39 |
-
return datos.get("respuestas", [])
|
| 40 |
-
except Exception:
|
| 41 |
-
return []
|
| 42 |
-
|
| 43 |
-
RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
|
| 44 |
-
|
| 45 |
-
def buscar_respuesta_personalizada(mensaje):
|
| 46 |
-
texto = mensaje.lower().strip()
|
| 47 |
-
for entrada in RESPUESTAS_PERSONALIZADAS:
|
| 48 |
-
for pregunta in entrada.get("preguntas", []):
|
| 49 |
-
if pregunta.lower() in texto or texto in pregunta.lower():
|
| 50 |
-
return entrada.get("respuesta")
|
| 51 |
-
return None
|
| 52 |
-
|
| 53 |
-
def generar_explicacion_juego(datos):
|
| 54 |
-
nombre = datos.get("nombre", "Este juego")
|
| 55 |
-
tema = datos.get("tema", "")
|
| 56 |
-
descripcion = datos.get("descripcion", "").strip()
|
| 57 |
-
visitas = datos.get("visitas", 0)
|
| 58 |
-
creador = datos.get("creador", "")
|
| 59 |
-
|
| 60 |
-
tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
|
| 61 |
-
|
| 62 |
-
partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
|
| 63 |
-
|
| 64 |
-
if descripcion and descripcion != "Sin descripción.":
|
| 65 |
-
desc_corta = descripcion[:200] + ("..." if len(descripcion) > 200 else "")
|
| 66 |
-
partes.append(f"Según su descripción oficial: *\"{desc_corta}\"*")
|
| 67 |
-
|
| 68 |
-
try:
|
| 69 |
-
v = int(visitas)
|
| 70 |
-
if v >= 1_000_000_000:
|
| 71 |
-
partes.append(f"Es uno de los juegos más visitados de Roblox con más de {v // 1_000_000_000} mil millones de visitas.")
|
| 72 |
-
elif v >= 1_000_000:
|
| 73 |
-
partes.append(f"Cuenta con más de {v // 1_000_000} millones de visitas en total.")
|
| 74 |
-
elif v >= 1_000:
|
| 75 |
-
partes.append(f"Cuenta con más de {v // 1_000}K visitas en total.")
|
| 76 |
-
except Exception:
|
| 77 |
-
pass
|
| 78 |
-
|
| 79 |
-
return " ".join(partes)
|
| 80 |
-
|
| 81 |
-
PATRONES_JUGADOR = [
|
| 82 |
-
r"buscar\s+jugador\s+(.+)",
|
| 83 |
-
r"busca\s+jugador\s+(.+)",
|
| 84 |
-
r"jugador\s+de\s+roblox\s+(.+)",
|
| 85 |
-
r"usuario\s+de\s+roblox\s+(.+)",
|
| 86 |
-
r"perfil\s+de\s+roblox\s+(.+)",
|
| 87 |
-
r"buscar\s+usuario\s+(.+)",
|
| 88 |
-
r"busca\s+usuario\s+(.+)",
|
| 89 |
-
r"quien\s+es\s+(.+)\s+en\s+roblox",
|
| 90 |
-
r"quién\s+es\s+(.+)\s+en\s+roblox",
|
| 91 |
-
r"info\s+de\s+(.+)\s+roblox",
|
| 92 |
-
]
|
| 93 |
-
|
| 94 |
-
PATRONES_JUEGO = [
|
| 95 |
-
r"buscar\s+juego\s+(.+)",
|
| 96 |
-
r"busca\s+juego\s+(.+)",
|
| 97 |
-
r"juego\s+de\s+roblox\s+(.+)",
|
| 98 |
-
r"buscar\s+(.+)\s+en\s+roblox",
|
| 99 |
-
r"busca\s+(.+)\s+en\s+roblox",
|
| 100 |
-
r"información\s+del\s+juego\s+(.+)",
|
| 101 |
-
r"informacion\s+del\s+juego\s+(.+)",
|
| 102 |
-
]
|
| 103 |
-
|
| 104 |
-
def detectar_roblox(mensaje):
|
| 105 |
-
texto = mensaje.lower().strip()
|
| 106 |
-
for patron in PATRONES_JUGADOR:
|
| 107 |
-
m = re.search(patron, texto)
|
| 108 |
-
if m:
|
| 109 |
-
return "jugador", m.group(1).strip()
|
| 110 |
-
for patron in PATRONES_JUEGO:
|
| 111 |
-
m = re.search(patron, texto)
|
| 112 |
-
if m:
|
| 113 |
-
return "juego", m.group(1).strip()
|
| 114 |
-
return None, None
|
| 115 |
-
|
| 116 |
-
def extraer_texto_content(content):
|
| 117 |
-
if not content:
|
| 118 |
-
return ""
|
| 119 |
-
if isinstance(content, str):
|
| 120 |
-
return content
|
| 121 |
-
if isinstance(content, list):
|
| 122 |
-
partes = []
|
| 123 |
-
for bloque in content:
|
| 124 |
-
if isinstance(bloque, str):
|
| 125 |
-
partes.append(bloque)
|
| 126 |
-
elif isinstance(bloque, dict):
|
| 127 |
-
partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
|
| 128 |
-
return " ".join(partes)
|
| 129 |
-
return str(content)
|
| 130 |
-
|
| 131 |
-
def modo_calculadora_activo(historial):
|
| 132 |
-
if not historial:
|
| 133 |
-
return False
|
| 134 |
-
for msg in reversed(historial):
|
| 135 |
-
if msg.get("role") == "assistant":
|
| 136 |
-
texto = extraer_texto_content(msg.get("content")).lower()
|
| 137 |
-
return "calculadora neo-1" in texto or "aquí tienes nuestra calculadora" in texto
|
| 138 |
-
return False
|
| 139 |
-
|
| 140 |
def añadir_turno(historial, user_msg, bot_msg=""):
|
| 141 |
return historial + [
|
| 142 |
{"role": "user", "content": user_msg},
|
|
@@ -199,11 +74,9 @@ def responder(mensaje, historial):
|
|
| 199 |
yield historial, ""
|
| 200 |
datos = buscar_juego(nombre_roblox)
|
| 201 |
resultado = formatear_juego(datos)
|
| 202 |
-
|
| 203 |
if datos and "error" not in datos:
|
| 204 |
explicacion = generar_explicacion_juego(datos)
|
| 205 |
resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
|
| 206 |
-
|
| 207 |
historial[-1]["content"] = resultado
|
| 208 |
yield historial, ""
|
| 209 |
return
|
|
@@ -259,22 +132,13 @@ with gr.Blocks(title="mdfjbots-neo-1") as demo:
|
|
| 259 |
label="Ejemplos de preguntas",
|
| 260 |
)
|
| 261 |
|
| 262 |
-
entrada.submit(
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
outputs=[chatbot, entrada],
|
| 266 |
-
)
|
| 267 |
-
btn_enviar.click(
|
| 268 |
-
fn=responder,
|
| 269 |
-
inputs=[entrada, chatbot],
|
| 270 |
-
outputs=[chatbot, entrada],
|
| 271 |
-
)
|
| 272 |
-
btn_limpiar.click(
|
| 273 |
-
fn=lambda: ([], ""),
|
| 274 |
-
outputs=[chatbot, entrada],
|
| 275 |
-
)
|
| 276 |
|
| 277 |
if __name__ == "__main__":
|
|
|
|
|
|
|
| 278 |
port = int(os.environ.get("PORT", 5000))
|
| 279 |
dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
|
| 280 |
root_path = f"https://{dev_domain}/__neo1" if dev_domain else ""
|
|
|
|
| 1 |
import os
|
|
|
|
|
|
|
| 2 |
import time
|
| 3 |
import gradio as gr
|
| 4 |
+
from neo_rest import iniciar_servidor
|
| 5 |
+
from logica import (
|
| 6 |
+
buscar_respuesta_personalizada, generar_explicacion_juego,
|
| 7 |
+
detectar_roblox, modo_calculadora_activo, extraer_texto_content,
|
| 8 |
+
)
|
| 9 |
from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
|
| 10 |
from matematicas import (
|
| 11 |
es_solicitud_calculadora, es_operacion_matematica,
|
| 12 |
+
resolver_operacion, formatear_resultado, extraer_nombre_usuario,
|
| 13 |
)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def añadir_turno(historial, user_msg, bot_msg=""):
|
| 16 |
return historial + [
|
| 17 |
{"role": "user", "content": user_msg},
|
|
|
|
| 74 |
yield historial, ""
|
| 75 |
datos = buscar_juego(nombre_roblox)
|
| 76 |
resultado = formatear_juego(datos)
|
|
|
|
| 77 |
if datos and "error" not in datos:
|
| 78 |
explicacion = generar_explicacion_juego(datos)
|
| 79 |
resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
|
|
|
|
| 80 |
historial[-1]["content"] = resultado
|
| 81 |
yield historial, ""
|
| 82 |
return
|
|
|
|
| 132 |
label="Ejemplos de preguntas",
|
| 133 |
)
|
| 134 |
|
| 135 |
+
entrada.submit(fn=responder, inputs=[entrada, chatbot], outputs=[chatbot, entrada])
|
| 136 |
+
btn_enviar.click(fn=responder, inputs=[entrada, chatbot], outputs=[chatbot, entrada])
|
| 137 |
+
btn_limpiar.click(fn=lambda: ([], ""), outputs=[chatbot, entrada])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
if __name__ == "__main__":
|
| 140 |
+
iniciar_servidor()
|
| 141 |
+
|
| 142 |
port = int(os.environ.get("PORT", 5000))
|
| 143 |
dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
|
| 144 |
root_path = f"https://{dev_domain}/__neo1" if dev_domain else ""
|
chat-app/logica.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
|
| 5 |
+
from matematicas import (
|
| 6 |
+
es_solicitud_calculadora, es_operacion_matematica,
|
| 7 |
+
resolver_operacion, formatear_resultado, extraer_nombre_usuario,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
GENEROS_ROBLOX = {
|
| 11 |
+
"Town and City": "un juego de vida urbana y roleplay social",
|
| 12 |
+
"Adventure": "un juego de aventura y exploración",
|
| 13 |
+
"Role Playing": "un juego de rol donde puedes ser quien quieras",
|
| 14 |
+
"Comedy": "un juego de comedia y entretenimiento",
|
| 15 |
+
"Action": "un juego de acción y combate",
|
| 16 |
+
"Horror": "un juego de terror y suspenso",
|
| 17 |
+
"Military": "un juego de temática militar y estrategia",
|
| 18 |
+
"Medieval": "un juego de temática medieval con castillos y caballeros",
|
| 19 |
+
"Naval": "un juego de aventuras navales y piratas",
|
| 20 |
+
"Sci-Fi": "un juego de ciencia ficción",
|
| 21 |
+
"Sports": "un juego deportivo",
|
| 22 |
+
"Fighting": "un juego de peleas y combate",
|
| 23 |
+
"Western": "un juego de temática del lejano oeste",
|
| 24 |
+
"FPS": "un juego de disparos en primera persona",
|
| 25 |
+
"Building": "un juego de construcción y creatividad",
|
| 26 |
+
"Simulator": "un simulador",
|
| 27 |
+
"Tycoon": "un juego de gestión y negocios (tycoon)",
|
| 28 |
+
"Obby": "un juego de obstáculos (obby)",
|
| 29 |
+
"All Genres": "un juego variado",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def cargar_respuestas():
|
| 33 |
+
ruta = os.path.join(os.path.dirname(__file__), "respuestas.json")
|
| 34 |
+
try:
|
| 35 |
+
with open(ruta, "r", encoding="utf-8") as f:
|
| 36 |
+
datos = json.load(f)
|
| 37 |
+
return datos.get("respuestas", [])
|
| 38 |
+
except Exception:
|
| 39 |
+
return []
|
| 40 |
+
|
| 41 |
+
RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
|
| 42 |
+
|
| 43 |
+
def buscar_respuesta_personalizada(mensaje):
|
| 44 |
+
texto = mensaje.lower().strip()
|
| 45 |
+
for entrada in RESPUESTAS_PERSONALIZADAS:
|
| 46 |
+
for pregunta in entrada.get("preguntas", []):
|
| 47 |
+
if pregunta.lower() in texto or texto in pregunta.lower():
|
| 48 |
+
return entrada.get("respuesta")
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
def generar_explicacion_juego(datos):
|
| 52 |
+
nombre = datos.get("nombre", "Este juego")
|
| 53 |
+
tema = datos.get("tema", "")
|
| 54 |
+
descripcion = datos.get("descripcion", "").strip()
|
| 55 |
+
visitas = datos.get("visitas", 0)
|
| 56 |
+
creador = datos.get("creador", "")
|
| 57 |
+
|
| 58 |
+
tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
|
| 59 |
+
partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
|
| 60 |
+
|
| 61 |
+
if descripcion and descripcion != "Sin descripción.":
|
| 62 |
+
desc_corta = descripcion[:200] + ("..." if len(descripcion) > 200 else "")
|
| 63 |
+
partes.append(f"Según su descripción oficial: *\"{desc_corta}\"*")
|
| 64 |
+
|
| 65 |
+
try:
|
| 66 |
+
v = int(visitas)
|
| 67 |
+
if v >= 1_000_000_000:
|
| 68 |
+
partes.append(f"Es uno de los juegos más visitados de Roblox con más de {v // 1_000_000_000} mil millones de visitas.")
|
| 69 |
+
elif v >= 1_000_000:
|
| 70 |
+
partes.append(f"Cuenta con más de {v // 1_000_000} millones de visitas en total.")
|
| 71 |
+
elif v >= 1_000:
|
| 72 |
+
partes.append(f"Cuenta con más de {v // 1_000}K visitas en total.")
|
| 73 |
+
except Exception:
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
return " ".join(partes)
|
| 77 |
+
|
| 78 |
+
PATRONES_JUGADOR = [
|
| 79 |
+
r"buscar\s+jugador\s+(.+)",
|
| 80 |
+
r"busca\s+jugador\s+(.+)",
|
| 81 |
+
r"jugador\s+de\s+roblox\s+(.+)",
|
| 82 |
+
r"usuario\s+de\s+roblox\s+(.+)",
|
| 83 |
+
r"perfil\s+de\s+roblox\s+(.+)",
|
| 84 |
+
r"buscar\s+usuario\s+(.+)",
|
| 85 |
+
r"busca\s+usuario\s+(.+)",
|
| 86 |
+
r"quien\s+es\s+(.+)\s+en\s+roblox",
|
| 87 |
+
r"quién\s+es\s+(.+)\s+en\s+roblox",
|
| 88 |
+
r"info\s+de\s+(.+)\s+roblox",
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
PATRONES_JUEGO = [
|
| 92 |
+
r"buscar\s+juego\s+(.+)",
|
| 93 |
+
r"busca\s+juego\s+(.+)",
|
| 94 |
+
r"juego\s+de\s+roblox\s+(.+)",
|
| 95 |
+
r"buscar\s+(.+)\s+en\s+roblox",
|
| 96 |
+
r"busca\s+(.+)\s+en\s+roblox",
|
| 97 |
+
r"información\s+del\s+juego\s+(.+)",
|
| 98 |
+
r"informacion\s+del\s+juego\s+(.+)",
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
def detectar_roblox(mensaje):
|
| 102 |
+
texto = mensaje.lower().strip()
|
| 103 |
+
for patron in PATRONES_JUGADOR:
|
| 104 |
+
m = re.search(patron, texto)
|
| 105 |
+
if m:
|
| 106 |
+
return "jugador", m.group(1).strip()
|
| 107 |
+
for patron in PATRONES_JUEGO:
|
| 108 |
+
m = re.search(patron, texto)
|
| 109 |
+
if m:
|
| 110 |
+
return "juego", m.group(1).strip()
|
| 111 |
+
return None, None
|
| 112 |
+
|
| 113 |
+
def extraer_texto_content(content):
|
| 114 |
+
if not content:
|
| 115 |
+
return ""
|
| 116 |
+
if isinstance(content, str):
|
| 117 |
+
return content
|
| 118 |
+
if isinstance(content, list):
|
| 119 |
+
partes = []
|
| 120 |
+
for bloque in content:
|
| 121 |
+
if isinstance(bloque, str):
|
| 122 |
+
partes.append(bloque)
|
| 123 |
+
elif isinstance(bloque, dict):
|
| 124 |
+
partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
|
| 125 |
+
return " ".join(partes)
|
| 126 |
+
return str(content)
|
| 127 |
+
|
| 128 |
+
def modo_calculadora_activo(historial):
|
| 129 |
+
if not historial:
|
| 130 |
+
return False
|
| 131 |
+
for msg in reversed(historial):
|
| 132 |
+
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 133 |
+
texto = extraer_texto_content(msg.get("content")).lower()
|
| 134 |
+
return "calculadora neo-1" in texto or "aquí tienes nuestra calculadora" in texto
|
| 135 |
+
return False
|
| 136 |
+
|
| 137 |
+
def respuesta_final(mensaje, historial):
|
| 138 |
+
texto = mensaje.strip().lower()
|
| 139 |
+
|
| 140 |
+
if es_solicitud_calculadora(mensaje):
|
| 141 |
+
nombre = extraer_nombre_usuario(historial)
|
| 142 |
+
return (
|
| 143 |
+
f"Claro 😀, {nombre} aquí tienes nuestra calculadora:\n\n"
|
| 144 |
+
"🧮 **Calculadora Virtual NEO-1**\n"
|
| 145 |
+
"Escribe cualquier operación matemática y la resolveré al instante.\n\n"
|
| 146 |
+
"**Ejemplos:** `5 + 3`, `12 * 7`, `100 / 4`, `2 ** 8` (potencia)\n\n"
|
| 147 |
+
"_Escribe tu operación o di 'salir de calculadora' para volver._"
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
if texto in ("salir de calculadora", "salir calculadora", "cerrar calculadora", "volver al chat"):
|
| 151 |
+
return "De acuerdo, volviendo al chat normal. ¡Pregúntame lo que quieras! 😊"
|
| 152 |
+
|
| 153 |
+
if modo_calculadora_activo(historial) or es_operacion_matematica(mensaje):
|
| 154 |
+
resultado = resolver_operacion(mensaje)
|
| 155 |
+
if resultado is not None:
|
| 156 |
+
return formatear_resultado(mensaje, resultado)
|
| 157 |
+
|
| 158 |
+
tipo_roblox, nombre_roblox = detectar_roblox(mensaje)
|
| 159 |
+
|
| 160 |
+
if tipo_roblox == "jugador":
|
| 161 |
+
datos = buscar_jugador(nombre_roblox)
|
| 162 |
+
return formatear_jugador(datos)
|
| 163 |
+
|
| 164 |
+
if tipo_roblox == "juego":
|
| 165 |
+
datos = buscar_juego(nombre_roblox)
|
| 166 |
+
resultado = formatear_juego(datos)
|
| 167 |
+
if datos and "error" not in datos:
|
| 168 |
+
explicacion = generar_explicacion_juego(datos)
|
| 169 |
+
resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
|
| 170 |
+
return resultado
|
| 171 |
+
|
| 172 |
+
respuesta = buscar_respuesta_personalizada(mensaje)
|
| 173 |
+
if respuesta:
|
| 174 |
+
return respuesta
|
| 175 |
+
|
| 176 |
+
return "🤖 NEO-1 aún no tiene una respuesta para eso. Puedes enseñarle más temas agregando respuestas al modelo."
|
chat-app/neo_rest.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import threading
|
| 3 |
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
sys.path.insert(0, os.path.dirname(__file__))
|
| 8 |
+
from logica import respuesta_final
|
| 9 |
+
|
| 10 |
+
NEO_REST_PORT = 5001
|
| 11 |
+
|
| 12 |
+
class NeoAPIHandler(BaseHTTPRequestHandler):
|
| 13 |
+
def _send_json(self, code, data):
|
| 14 |
+
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
|
| 15 |
+
self.send_response(code)
|
| 16 |
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
| 17 |
+
self.send_header("Content-Length", str(len(body)))
|
| 18 |
+
self.end_headers()
|
| 19 |
+
self.wfile.write(body)
|
| 20 |
+
|
| 21 |
+
def do_POST(self):
|
| 22 |
+
if self.path == "/chat":
|
| 23 |
+
try:
|
| 24 |
+
length = int(self.headers.get("Content-Length", 0))
|
| 25 |
+
raw = self.rfile.read(length)
|
| 26 |
+
data = json.loads(raw)
|
| 27 |
+
|
| 28 |
+
mensaje = str(data.get("message", "")).strip()
|
| 29 |
+
historial = data.get("history", [])
|
| 30 |
+
|
| 31 |
+
if not mensaje:
|
| 32 |
+
self._send_json(400, {"error": "El campo 'message' es obligatorio"})
|
| 33 |
+
return
|
| 34 |
+
|
| 35 |
+
respuesta = respuesta_final(mensaje, historial)
|
| 36 |
+
self._send_json(200, {
|
| 37 |
+
"response": respuesta,
|
| 38 |
+
"model": "mdfjbots-neo-1",
|
| 39 |
+
"status": "ok",
|
| 40 |
+
})
|
| 41 |
+
except json.JSONDecodeError:
|
| 42 |
+
self._send_json(400, {"error": "JSON inválido"})
|
| 43 |
+
except Exception as e:
|
| 44 |
+
self._send_json(500, {"error": str(e)})
|
| 45 |
+
else:
|
| 46 |
+
self._send_json(404, {"error": "Ruta no encontrada"})
|
| 47 |
+
|
| 48 |
+
def do_GET(self):
|
| 49 |
+
if self.path == "/health":
|
| 50 |
+
self._send_json(200, {"status": "ok", "model": "mdfjbots-neo-1"})
|
| 51 |
+
else:
|
| 52 |
+
self._send_json(404, {"error": "Ruta no encontrada"})
|
| 53 |
+
|
| 54 |
+
def log_message(self, format, *args):
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
def iniciar_servidor():
|
| 58 |
+
server = HTTPServer(("0.0.0.0", NEO_REST_PORT), NeoAPIHandler)
|
| 59 |
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
| 60 |
+
thread.start()
|
| 61 |
+
print(f"[NeoAPI REST] Corriendo en http://0.0.0.0:{NEO_REST_PORT}")
|
| 62 |
+
return server
|
neo-api-client.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* NeoAPI Client — mdfjbots-neo-1
|
| 3 |
+
* Úsalo en tu página web para conectarte con la NeoAPI.
|
| 4 |
+
*
|
| 5 |
+
* Uso básico:
|
| 6 |
+
* const neo = new NeoAPI("neo1-tu_api_key_aqui");
|
| 7 |
+
* const respuesta = await neo.chat("Hola, ¿quién eres?");
|
| 8 |
+
* console.log(respuesta);
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
class NeoAPI {
|
| 12 |
+
/**
|
| 13 |
+
* @param {string} apiKey - Tu API key (formato: neo1-...)
|
| 14 |
+
* @param {string} [baseUrl] - URL base de la NeoAPI (opcional, por defecto la URL de producción)
|
| 15 |
+
*/
|
| 16 |
+
constructor(apiKey, baseUrl = "https://TU_DOMINIO_AQUI/api") {
|
| 17 |
+
if (!apiKey) throw new Error("NeoAPI: se requiere una API key.");
|
| 18 |
+
this.apiKey = apiKey;
|
| 19 |
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
| 20 |
+
this.history = [];
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Envía un mensaje al modelo NEO-1 y obtiene la respuesta.
|
| 25 |
+
* El historial de conversación se mantiene automáticamente.
|
| 26 |
+
*
|
| 27 |
+
* @param {string} message - El mensaje del usuario
|
| 28 |
+
* @returns {Promise<string>} - La respuesta del modelo
|
| 29 |
+
*/
|
| 30 |
+
async chat(message) {
|
| 31 |
+
const response = await fetch(`${this.baseUrl}/neo/chat`, {
|
| 32 |
+
method: "POST",
|
| 33 |
+
headers: {
|
| 34 |
+
"Content-Type": "application/json",
|
| 35 |
+
Authorization: `Bearer ${this.apiKey}`,
|
| 36 |
+
},
|
| 37 |
+
body: JSON.stringify({
|
| 38 |
+
message,
|
| 39 |
+
history: this.history,
|
| 40 |
+
}),
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
if (!response.ok) {
|
| 44 |
+
const err = await response.json().catch(() => ({ error: response.statusText }));
|
| 45 |
+
throw new Error(`NeoAPI error ${response.status}: ${err.error ?? response.statusText}`);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const data = await response.json();
|
| 49 |
+
const botReply = data.response;
|
| 50 |
+
|
| 51 |
+
this.history.push(
|
| 52 |
+
{ role: "user", content: message },
|
| 53 |
+
{ role: "assistant", content: botReply }
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
return botReply;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Envía un mensaje sin mantener historial (cada llamada es independiente).
|
| 61 |
+
*
|
| 62 |
+
* @param {string} message - El mensaje del usuario
|
| 63 |
+
* @param {Array} [history] - Historial opcional a enviar
|
| 64 |
+
* @returns {Promise<string>}
|
| 65 |
+
*/
|
| 66 |
+
async chatOnce(message, history = []) {
|
| 67 |
+
const response = await fetch(`${this.baseUrl}/neo/chat`, {
|
| 68 |
+
method: "POST",
|
| 69 |
+
headers: {
|
| 70 |
+
"Content-Type": "application/json",
|
| 71 |
+
Authorization: `Bearer ${this.apiKey}`,
|
| 72 |
+
},
|
| 73 |
+
body: JSON.stringify({ message, history }),
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
if (!response.ok) {
|
| 77 |
+
const err = await response.json().catch(() => ({ error: response.statusText }));
|
| 78 |
+
throw new Error(`NeoAPI error ${response.status}: ${err.error ?? response.statusText}`);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
const data = await response.json();
|
| 82 |
+
return data.response;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/** Limpia el historial de conversación. */
|
| 86 |
+
clearHistory() {
|
| 87 |
+
this.history = [];
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/** Obtiene los modelos disponibles. */
|
| 91 |
+
async getModels() {
|
| 92 |
+
const response = await fetch(`${this.baseUrl}/neo/models`, {
|
| 93 |
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
| 94 |
+
});
|
| 95 |
+
if (!response.ok) throw new Error("NeoAPI: error al obtener modelos.");
|
| 96 |
+
return response.json();
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/**
|
| 101 |
+
* ─────────────────────────────────────────────
|
| 102 |
+
* EJEMPLOS DE USO
|
| 103 |
+
* ─────────────────────────────────────────────
|
| 104 |
+
*
|
| 105 |
+
* 1) Conversación simple:
|
| 106 |
+
*
|
| 107 |
+
* const neo = new NeoAPI("neo1-tu_api_key");
|
| 108 |
+
* const res = await neo.chat("¿Qué es Linux?");
|
| 109 |
+
* document.getElementById("respuesta").textContent = res;
|
| 110 |
+
*
|
| 111 |
+
*
|
| 112 |
+
* 2) Chatbot con historial en la página:
|
| 113 |
+
*
|
| 114 |
+
* const neo = new NeoAPI("neo1-tu_api_key");
|
| 115 |
+
*
|
| 116 |
+
* async function enviar() {
|
| 117 |
+
* const input = document.getElementById("input").value;
|
| 118 |
+
* const res = await neo.chat(input);
|
| 119 |
+
* mostrarMensaje("NEO-1", res);
|
| 120 |
+
* }
|
| 121 |
+
*
|
| 122 |
+
*
|
| 123 |
+
* 3) Llamada directa sin historial (para búsquedas puntuales):
|
| 124 |
+
*
|
| 125 |
+
* const neo = new NeoAPI("neo1-tu_api_key");
|
| 126 |
+
* const res = await neo.chatOnce("buscar juego Adopt Me");
|
| 127 |
+
* console.log(res);
|
| 128 |
+
*
|
| 129 |
+
*
|
| 130 |
+
* ─────────────────────────────────────────────
|
| 131 |
+
* ENDPOINTS DE ADMIN (para gestionar API keys)
|
| 132 |
+
* Se hacen desde tu backend, nunca desde el frontend.
|
| 133 |
+
* ─────────────────────────────────────────────
|
| 134 |
+
*
|
| 135 |
+
* Crear una nueva key:
|
| 136 |
+
* POST /api/keys
|
| 137 |
+
* Header: x-admin-secret: TU_ADMIN_SECRET
|
| 138 |
+
* Body: { "name": "mi-app-web" }
|
| 139 |
+
*
|
| 140 |
+
* Listar keys:
|
| 141 |
+
* GET /api/keys
|
| 142 |
+
* Header: x-admin-secret: TU_ADMIN_SECRET
|
| 143 |
+
*
|
| 144 |
+
* Revocar una key:
|
| 145 |
+
* DELETE /api/keys/mi-app-web
|
| 146 |
+
* Header: x-admin-secret: TU_ADMIN_SECRET
|
| 147 |
+
*/
|
| 148 |
+
|
| 149 |
+
export default NeoAPI;
|