bentosmau commited on
Commit ยท
9fae0c6
1
Parent(s): 700f4b5
Translate all code comments, strings, and variable names to English
Browse filesUpdates comments, docstrings, user-facing strings, variable names, and function names across Python and TypeScript files to English.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e3ff2484-bbd8-4aba-bea0-1940769b874a
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fca1b854-e4b4-4bfc-9bb2-ebe5af51a4f9
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1739408b-93a5-479b-a658-30f2493b0467/e3ff2484-bbd8-4aba-bea0-1940769b874a/Pw9jNbA
Replit-Helium-Checkpoint-Created: true
- artifacts/api-server/src/middlewares/auth.ts +3 -3
- artifacts/api-server/src/routes/keys.ts +6 -6
- artifacts/api-server/src/routes/neo.ts +9 -9
- chat-app/app.py +114 -115
- chat-app/buscador.py +134 -137
- chat-app/compilar_respuestas.py +54 -52
- chat-app/logica.py +261 -247
- chat-app/matematicas.py +68 -46
- chat-app/neo_rest.py +17 -17
- chat-app/resumidor.py +112 -123
- chat-app/roblox_api.py +131 -115
artifacts/api-server/src/middlewares/auth.ts
CHANGED
|
@@ -8,13 +8,13 @@ export function requireApiKey(req: Request, res: Response, next: NextFunction) {
|
|
| 8 |
: (req.headers["x-api-key"] as string | undefined)?.trim() ?? "";
|
| 9 |
|
| 10 |
if (!key) {
|
| 11 |
-
res.status(401).json({ error: "API key
|
| 12 |
return;
|
| 13 |
}
|
| 14 |
|
| 15 |
const found = validateKey(key);
|
| 16 |
if (!found) {
|
| 17 |
-
res.status(403).json({ error: "
|
| 18 |
return;
|
| 19 |
}
|
| 20 |
|
|
@@ -25,7 +25,7 @@ export function requireApiKey(req: Request, res: Response, next: NextFunction) {
|
|
| 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: "
|
| 29 |
return;
|
| 30 |
}
|
| 31 |
next();
|
|
|
|
| 8 |
: (req.headers["x-api-key"] as string | undefined)?.trim() ?? "";
|
| 9 |
|
| 10 |
if (!key) {
|
| 11 |
+
res.status(401).json({ error: "API key required. Use the header: Authorization: Bearer <your-key>" });
|
| 12 |
return;
|
| 13 |
}
|
| 14 |
|
| 15 |
const found = validateKey(key);
|
| 16 |
if (!found) {
|
| 17 |
+
res.status(403).json({ error: "Invalid or revoked API key." });
|
| 18 |
return;
|
| 19 |
}
|
| 20 |
|
|
|
|
| 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: "Incorrect admin secret." });
|
| 29 |
return;
|
| 30 |
}
|
| 31 |
next();
|
artifacts/api-server/src/routes/keys.ts
CHANGED
|
@@ -7,14 +7,14 @@ const router = Router();
|
|
| 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: "
|
| 11 |
return;
|
| 12 |
}
|
| 13 |
const key = generateKey(name.trim());
|
| 14 |
res.status(201).json({
|
| 15 |
-
message:
|
| 16 |
-
key:
|
| 17 |
-
name:
|
| 18 |
createdAt: key.createdAt,
|
| 19 |
});
|
| 20 |
});
|
|
@@ -27,10 +27,10 @@ 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
|
| 31 |
return;
|
| 32 |
}
|
| 33 |
-
res.json({ message: `Key '${name}'
|
| 34 |
});
|
| 35 |
|
| 36 |
export default router;
|
|
|
|
| 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: "The 'name' field is required." });
|
| 11 |
return;
|
| 12 |
}
|
| 13 |
const key = generateKey(name.trim());
|
| 14 |
res.status(201).json({
|
| 15 |
+
message: "API key generated. Save it now โ it won't be shown again.",
|
| 16 |
+
key: key.key,
|
| 17 |
+
name: key.name,
|
| 18 |
createdAt: key.createdAt,
|
| 19 |
});
|
| 20 |
});
|
|
|
|
| 27 |
const { name } = req.params;
|
| 28 |
const ok = revokeKey(name!);
|
| 29 |
if (!ok) {
|
| 30 |
+
res.status(404).json({ error: "Key not found or already revoked." });
|
| 31 |
return;
|
| 32 |
}
|
| 33 |
+
res.json({ message: `Key '${name}' revoked.` });
|
| 34 |
});
|
| 35 |
|
| 36 |
export default router;
|
artifacts/api-server/src/routes/neo.ts
CHANGED
|
@@ -11,7 +11,7 @@ router.post("/neo/chat", requireApiKey, async (req, res) => {
|
|
| 11 |
};
|
| 12 |
|
| 13 |
if (!message || typeof message !== "string" || !message.trim()) {
|
| 14 |
-
res.status(400).json({ error: "
|
| 15 |
return;
|
| 16 |
}
|
| 17 |
|
|
@@ -24,19 +24,19 @@ router.post("/neo/chat", requireApiKey, async (req, res) => {
|
|
| 24 |
|
| 25 |
if (!response.ok) {
|
| 26 |
const err = await response.text();
|
| 27 |
-
res.status(502).json({ error: "
|
| 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:
|
| 35 |
-
status:
|
| 36 |
});
|
| 37 |
} catch (err: any) {
|
| 38 |
res.status(503).json({
|
| 39 |
-
error:
|
| 40 |
detail: err?.message ?? String(err),
|
| 41 |
});
|
| 42 |
}
|
|
@@ -46,10 +46,10 @@ router.get("/neo/models", requireApiKey, (_req, res) => {
|
|
| 46 |
res.json({
|
| 47 |
models: [
|
| 48 |
{
|
| 49 |
-
id:
|
| 50 |
-
name:
|
| 51 |
-
description: "
|
| 52 |
-
version:
|
| 53 |
},
|
| 54 |
],
|
| 55 |
});
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
if (!message || typeof message !== "string" || !message.trim()) {
|
| 14 |
+
res.status(400).json({ error: "The 'message' field is required." });
|
| 15 |
return;
|
| 16 |
}
|
| 17 |
|
|
|
|
| 24 |
|
| 25 |
if (!response.ok) {
|
| 26 |
const err = await response.text();
|
| 27 |
+
res.status(502).json({ error: "NEO-1 model error.", 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: "Could not connect to the NEO-1 model. Is it running?",
|
| 40 |
detail: err?.message ?? String(err),
|
| 41 |
});
|
| 42 |
}
|
|
|
|
| 46 |
res.json({
|
| 47 |
models: [
|
| 48 |
{
|
| 49 |
+
id: "mdfjbots-neo-1",
|
| 50 |
+
name: "NEO-1",
|
| 51 |
+
description: "Conversational assistant with Roblox support, calculator, and educational topics.",
|
| 52 |
+
version: "1.13.1",
|
| 53 |
},
|
| 54 |
],
|
| 55 |
});
|
chat-app/app.py
CHANGED
|
@@ -1,158 +1,157 @@
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
import gradio as gr
|
| 4 |
-
from neo_rest import
|
| 5 |
from logica import (
|
| 6 |
-
|
| 7 |
-
|
| 8 |
)
|
| 9 |
-
from buscador import
|
| 10 |
-
from resumidor import
|
| 11 |
-
from roblox_api import
|
| 12 |
from matematicas import (
|
| 13 |
-
|
| 14 |
-
|
| 15 |
)
|
| 16 |
|
| 17 |
-
def
|
| 18 |
-
return
|
| 19 |
{"role": "user", "content": user_msg},
|
| 20 |
{"role": "assistant", "content": bot_msg},
|
| 21 |
]
|
| 22 |
|
| 23 |
-
def stream_tokens(
|
| 24 |
"""
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
"""
|
| 29 |
-
tokens =
|
| 30 |
-
|
| 31 |
for i, token in enumerate(tokens):
|
| 32 |
-
|
| 33 |
if i < len(tokens) - 1:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# Pausa variable: tokens largos tardan un poco mรกs (como un LLM real)
|
| 37 |
delay = 0.055 if len(token) > 4 else 0.030
|
| 38 |
time.sleep(delay)
|
| 39 |
-
yield
|
| 40 |
-
|
| 41 |
-
def
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
if
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
f"
|
| 48 |
-
"๐งฎ **
|
| 49 |
-
"
|
| 50 |
-
"**
|
| 51 |
"- `5 + 3`\n- `12 * 7`\n- `100 / 4`\n"
|
| 52 |
-
"- `2 ** 8` (
|
| 53 |
-
"
|
| 54 |
)
|
| 55 |
-
|
| 56 |
-
for h in stream_tokens(
|
| 57 |
yield h, ""
|
| 58 |
return
|
| 59 |
|
| 60 |
-
if
|
| 61 |
-
|
| 62 |
-
yield
|
| 63 |
return
|
| 64 |
|
| 65 |
-
if
|
| 66 |
-
|
| 67 |
-
if
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
for h in stream_tokens(
|
| 71 |
yield h, ""
|
| 72 |
return
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
if
|
| 77 |
-
|
| 78 |
-
yield
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
yield
|
| 83 |
return
|
| 84 |
|
| 85 |
-
if
|
| 86 |
-
|
| 87 |
-
yield
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
if
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
yield
|
| 95 |
return
|
| 96 |
|
| 97 |
-
|
| 98 |
-
if
|
| 99 |
-
|
| 100 |
-
for h in stream_tokens(
|
| 101 |
yield h, ""
|
| 102 |
return
|
| 103 |
|
| 104 |
-
# โโ NEO-2:
|
| 105 |
-
|
| 106 |
-
yield
|
| 107 |
|
| 108 |
-
|
| 109 |
|
| 110 |
-
if
|
| 111 |
-
|
| 112 |
-
if
|
| 113 |
-
|
| 114 |
-
for h in stream_tokens(
|
| 115 |
yield h, ""
|
| 116 |
return
|
| 117 |
|
| 118 |
-
# โโ
|
| 119 |
-
|
| 120 |
|
| 121 |
-
if
|
| 122 |
msg = (
|
| 123 |
-
"โ ๏ธ **
|
| 124 |
-
"
|
| 125 |
-
"
|
| 126 |
-
"
|
| 127 |
)
|
| 128 |
-
elif
|
| 129 |
msg = (
|
| 130 |
-
"โณ **
|
| 131 |
-
"
|
| 132 |
-
"
|
| 133 |
)
|
| 134 |
-
elif
|
| 135 |
msg = (
|
| 136 |
-
"๐ **
|
| 137 |
-
"
|
| 138 |
-
"(All Rights Reserved, CC BY-ND, etc.)
|
| 139 |
-
"
|
| 140 |
)
|
| 141 |
else:
|
| 142 |
msg = (
|
| 143 |
-
"๐ค **
|
| 144 |
-
"
|
| 145 |
-
"
|
| 146 |
)
|
| 147 |
|
| 148 |
-
|
| 149 |
-
yield
|
| 150 |
|
| 151 |
with gr.Blocks(title="mdfjbots-neo-1") as demo:
|
| 152 |
gr.Markdown(
|
| 153 |
"""
|
| 154 |
# ๐ค mdfjbots-neo-1
|
| 155 |
-
###
|
| 156 |
"""
|
| 157 |
)
|
| 158 |
|
|
@@ -163,38 +162,38 @@ with gr.Blocks(title="mdfjbots-neo-1") as demo:
|
|
| 163 |
)
|
| 164 |
|
| 165 |
with gr.Row():
|
| 166 |
-
|
| 167 |
-
placeholder="
|
| 168 |
show_label=False,
|
| 169 |
scale=9,
|
| 170 |
container=False,
|
| 171 |
autofocus=True,
|
| 172 |
)
|
| 173 |
-
|
| 174 |
|
| 175 |
with gr.Row():
|
| 176 |
-
|
| 177 |
|
| 178 |
gr.Examples(
|
| 179 |
examples=[
|
| 180 |
-
"
|
| 181 |
-
"
|
| 182 |
-
"
|
| 183 |
-
"
|
| 184 |
-
"
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
],
|
| 188 |
-
inputs=
|
| 189 |
-
label="
|
| 190 |
)
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
|
| 196 |
if __name__ == "__main__":
|
| 197 |
-
|
| 198 |
|
| 199 |
port = int(os.environ.get("PORT", 5000))
|
| 200 |
dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
|
|
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
import gradio as gr
|
| 4 |
+
from neo_rest import start_server
|
| 5 |
from logica import (
|
| 6 |
+
find_custom_response, generate_game_explanation,
|
| 7 |
+
detect_roblox, calculator_mode_active, extract_text_content,
|
| 8 |
)
|
| 9 |
+
from buscador import search as web_search, ERROR_NETWORK, ERROR_RATE_LIMIT, ERROR_LICENSE
|
| 10 |
+
from resumidor import summarize
|
| 11 |
+
from roblox_api import search_player, search_game, format_player, format_game
|
| 12 |
from matematicas import (
|
| 13 |
+
is_calculator_request, is_math_operation,
|
| 14 |
+
solve_operation, format_result, extract_username,
|
| 15 |
)
|
| 16 |
|
| 17 |
+
def add_turn(history, user_msg, bot_msg=""):
|
| 18 |
+
return history + [
|
| 19 |
{"role": "user", "content": user_msg},
|
| 20 |
{"role": "assistant", "content": bot_msg},
|
| 21 |
]
|
| 22 |
|
| 23 |
+
def stream_tokens(text, history):
|
| 24 |
"""
|
| 25 |
+
Emits the response token by token (word by word),
|
| 26 |
+
simulating the generation process of a real language model.
|
| 27 |
+
Longer tokens = slightly longer pauses (more semantic weight).
|
| 28 |
"""
|
| 29 |
+
tokens = text.split(" ")
|
| 30 |
+
accumulated = ""
|
| 31 |
for i, token in enumerate(tokens):
|
| 32 |
+
accumulated += token
|
| 33 |
if i < len(tokens) - 1:
|
| 34 |
+
accumulated += " "
|
| 35 |
+
history[-1]["content"] = accumulated
|
|
|
|
| 36 |
delay = 0.055 if len(token) > 4 else 0.030
|
| 37 |
time.sleep(delay)
|
| 38 |
+
yield history
|
| 39 |
+
|
| 40 |
+
def respond(message, history):
|
| 41 |
+
text = message.strip().lower()
|
| 42 |
+
|
| 43 |
+
if is_calculator_request(message):
|
| 44 |
+
name = extract_username(history)
|
| 45 |
+
greeting = (
|
| 46 |
+
f"Sure! ๐ {name}, here's our calculator:\n\n"
|
| 47 |
+
"๐งฎ **NEO-1 Virtual Calculator**\n"
|
| 48 |
+
"Type any math operation and I'll solve it instantly.\n\n"
|
| 49 |
+
"**Examples:**\n"
|
| 50 |
"- `5 + 3`\n- `12 * 7`\n- `100 / 4`\n"
|
| 51 |
+
"- `2 ** 8` (power)\n- `144 ** 0.5` (square root)\n\n"
|
| 52 |
+
"_Type your operation or say 'exit calculator' to go back._"
|
| 53 |
)
|
| 54 |
+
history = add_turn(history, message)
|
| 55 |
+
for h in stream_tokens(greeting, history):
|
| 56 |
yield h, ""
|
| 57 |
return
|
| 58 |
|
| 59 |
+
if text in ("exit calculator", "close calculator", "quit calculator", "back to chat"):
|
| 60 |
+
history = add_turn(history, message, "Alright, back to normal chat. Ask me anything! ๐")
|
| 61 |
+
yield history, ""
|
| 62 |
return
|
| 63 |
|
| 64 |
+
if calculator_mode_active(history) or is_math_operation(message):
|
| 65 |
+
result = solve_operation(message)
|
| 66 |
+
if result is not None:
|
| 67 |
+
response = format_result(message, result)
|
| 68 |
+
history = add_turn(history, message)
|
| 69 |
+
for h in stream_tokens(response, history):
|
| 70 |
yield h, ""
|
| 71 |
return
|
| 72 |
|
| 73 |
+
roblox_type, roblox_name = detect_roblox(message)
|
| 74 |
|
| 75 |
+
if roblox_type == "player":
|
| 76 |
+
history = add_turn(history, message, "๐ Searching for player on Roblox...")
|
| 77 |
+
yield history, ""
|
| 78 |
+
data = search_player(roblox_name)
|
| 79 |
+
result = format_player(data)
|
| 80 |
+
history[-1]["content"] = result
|
| 81 |
+
yield history, ""
|
| 82 |
return
|
| 83 |
|
| 84 |
+
if roblox_type == "game":
|
| 85 |
+
history = add_turn(history, message, "๐ Searching for game on Roblox...")
|
| 86 |
+
yield history, ""
|
| 87 |
+
data = search_game(roblox_name)
|
| 88 |
+
result = format_game(data)
|
| 89 |
+
if data and "error" not in data:
|
| 90 |
+
explanation = generate_game_explanation(data)
|
| 91 |
+
result = result + "\n\n๐ก **What is this game about?**\n" + explanation
|
| 92 |
+
history[-1]["content"] = result
|
| 93 |
+
yield history, ""
|
| 94 |
return
|
| 95 |
|
| 96 |
+
custom_response = find_custom_response(message)
|
| 97 |
+
if custom_response:
|
| 98 |
+
history = add_turn(history, message)
|
| 99 |
+
for h in stream_tokens(custom_response, history):
|
| 100 |
yield h, ""
|
| 101 |
return
|
| 102 |
|
| 103 |
+
# โโ NEO-2: web search fallback โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 104 |
+
history = add_turn(history, message, "๐ Searching the web...")
|
| 105 |
+
yield history, ""
|
| 106 |
|
| 107 |
+
web_result = web_search(message)
|
| 108 |
|
| 109 |
+
if web_result.get("found"):
|
| 110 |
+
summary = summarize(message, web_result)
|
| 111 |
+
if summary:
|
| 112 |
+
history[-1]["content"] = ""
|
| 113 |
+
for h in stream_tokens(summary, history):
|
| 114 |
yield h, ""
|
| 115 |
return
|
| 116 |
|
| 117 |
+
# โโ User-facing error messages based on failure type โโโโโโโโโโโโโโโโโโโโโโ
|
| 118 |
+
error_type = web_result.get("error_type", "")
|
| 119 |
|
| 120 |
+
if error_type == ERROR_NETWORK:
|
| 121 |
msg = (
|
| 122 |
+
"โ ๏ธ **No internet connection**\n\n"
|
| 123 |
+
"I couldn't reach the web search engine right now. "
|
| 124 |
+
"I also checked my knowledge base but found nothing on that topic.\n\n"
|
| 125 |
+
"_Please try again in a few seconds or rephrase your question._"
|
| 126 |
)
|
| 127 |
+
elif error_type == ERROR_RATE_LIMIT:
|
| 128 |
msg = (
|
| 129 |
+
"โณ **Too many searches in a short time**\n\n"
|
| 130 |
+
"The search engine asked me to slow down for a moment. "
|
| 131 |
+
"Please wait a few seconds and try again. ๐"
|
| 132 |
)
|
| 133 |
+
elif error_type == ERROR_LICENSE:
|
| 134 |
msg = (
|
| 135 |
+
"๐ **Sources unavailable**\n\n"
|
| 136 |
+
"I found results online, but all sources use restrictive licenses "
|
| 137 |
+
"(All Rights Reserved, CC BY-ND, etc.) that don't allow me to use their content.\n\n"
|
| 138 |
+
"_Try rephrasing your question to find open-licensed sources._"
|
| 139 |
)
|
| 140 |
else:
|
| 141 |
msg = (
|
| 142 |
+
"๐ค **No results found**\n\n"
|
| 143 |
+
"I couldn't find information on that topic in my knowledge base "
|
| 144 |
+
"or on the web. Try rephrasing your question."
|
| 145 |
)
|
| 146 |
|
| 147 |
+
history[-1]["content"] = msg
|
| 148 |
+
yield history, ""
|
| 149 |
|
| 150 |
with gr.Blocks(title="mdfjbots-neo-1") as demo:
|
| 151 |
gr.Markdown(
|
| 152 |
"""
|
| 153 |
# ๐ค mdfjbots-neo-1
|
| 154 |
+
### Conversational AI assistant
|
| 155 |
"""
|
| 156 |
)
|
| 157 |
|
|
|
|
| 162 |
)
|
| 163 |
|
| 164 |
with gr.Row():
|
| 165 |
+
input_box = gr.Textbox(
|
| 166 |
+
placeholder="Type your message here... (e.g. 'search player Builderman')",
|
| 167 |
show_label=False,
|
| 168 |
scale=9,
|
| 169 |
container=False,
|
| 170 |
autofocus=True,
|
| 171 |
)
|
| 172 |
+
btn_send = gr.Button("Send", scale=1, variant="primary")
|
| 173 |
|
| 174 |
with gr.Row():
|
| 175 |
+
btn_clear = gr.Button("๐๏ธ Clear conversation", size="sm")
|
| 176 |
|
| 177 |
gr.Examples(
|
| 178 |
examples=[
|
| 179 |
+
"Hello, who are you?",
|
| 180 |
+
"search player Builderman",
|
| 181 |
+
"search game Adopt Me",
|
| 182 |
+
"definition of history",
|
| 183 |
+
"calculator",
|
| 184 |
+
"what is linux",
|
| 185 |
+
"korean war",
|
| 186 |
],
|
| 187 |
+
inputs=input_box,
|
| 188 |
+
label="Example questions",
|
| 189 |
)
|
| 190 |
|
| 191 |
+
input_box.submit(fn=respond, inputs=[input_box, chatbot], outputs=[chatbot, input_box])
|
| 192 |
+
btn_send.click(fn=respond, inputs=[input_box, chatbot], outputs=[chatbot, input_box])
|
| 193 |
+
btn_clear.click(fn=lambda: ([], ""), outputs=[chatbot, input_box])
|
| 194 |
|
| 195 |
if __name__ == "__main__":
|
| 196 |
+
start_server()
|
| 197 |
|
| 198 |
port = int(os.environ.get("PORT", 5000))
|
| 199 |
dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
|
chat-app/buscador.py
CHANGED
|
@@ -1,70 +1,70 @@
|
|
| 1 |
"""
|
| 2 |
-
buscador.py โ NEO-2
|
| 3 |
-
------------------------------------------------------------
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
โ
|
| 9 |
-
โ
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
โข
|
| 13 |
-
โข
|
| 14 |
-
โข
|
| 15 |
"""
|
| 16 |
|
| 17 |
import time
|
| 18 |
from ddgs import DDGS
|
| 19 |
|
| 20 |
-
# โโ
|
| 21 |
-
#
|
| 22 |
_CACHE: dict[str, dict] = {}
|
| 23 |
-
_MAX_CACHE = 50
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
# โโ
|
| 29 |
|
| 30 |
-
# โ
|
| 31 |
-
|
| 32 |
-
#
|
| 33 |
-
"nasa.gov": "CC0 /
|
| 34 |
"data.gov": "CC0",
|
| 35 |
-
"datos.gob.mx": "
|
| 36 |
"datos.gob.es": "CC BY",
|
| 37 |
"datos.gob.ar": "CC BY",
|
| 38 |
"open.canada.ca": "CC BY",
|
| 39 |
"data.europa.eu": "CC BY 4.0",
|
| 40 |
"ec.europa.eu": "CC BY 4.0",
|
| 41 |
-
"who.int": "CC BY-NC-SA",
|
| 42 |
-
"un.org": "
|
| 43 |
|
| 44 |
-
#
|
| 45 |
"wikidata.org": "CC0",
|
| 46 |
"commons.wikimedia.org": "CC BY / CC0",
|
| 47 |
-
"gutenberg.org": "
|
| 48 |
-
"archive.org": "CC BY /
|
| 49 |
"openstax.org": "CC BY 4.0",
|
| 50 |
-
"arxiv.org": "CC BY /
|
| 51 |
"plos.org": "CC BY 4.0",
|
| 52 |
"doaj.org": "CC BY",
|
| 53 |
"creativecommons.org":"CC BY 4.0",
|
| 54 |
-
"publicdomainreview.org": "CC BY-SA",
|
| 55 |
"freesound.org": "CC0 / CC BY",
|
| 56 |
-
"unsplash.com": "
|
| 57 |
"pixabay.com": "CC0",
|
| 58 |
"pexels.com": "CC0",
|
| 59 |
|
| 60 |
-
#
|
| 61 |
-
"simple.wikipedia.org": "CC BY-SA",
|
| 62 |
"es.wikiversity.org": "CC BY-SA",
|
| 63 |
}
|
| 64 |
|
| 65 |
-
# โ
|
| 66 |
-
|
| 67 |
-
# CC BY-SA (share-alike โ
|
| 68 |
"wikipedia.org": "CC BY-SA",
|
| 69 |
"wikivoyage.org": "CC BY-SA",
|
| 70 |
"wikibooks.org": "CC BY-SA",
|
|
@@ -74,18 +74,18 @@ DOMINIOS_EVITADOS: dict[str, str] = {
|
|
| 74 |
"stackexchange.com": "CC BY-SA 4.0",
|
| 75 |
"openstreetmap.org": "ODbL (share-alike)",
|
| 76 |
|
| 77 |
-
# CC BY-NC (
|
| 78 |
"medium.com": "CC BY-NC",
|
| 79 |
"academia.edu": "CC BY-NC",
|
| 80 |
"researchgate.net": "CC BY-NC",
|
| 81 |
"slideshare.net": "CC BY-NC",
|
| 82 |
"ted.com": "CC BY-NC",
|
| 83 |
|
| 84 |
-
# CC BY-ND (
|
| 85 |
"economist.com": "CC BY-ND",
|
| 86 |
"foreignpolicy.com": "CC BY-ND",
|
| 87 |
|
| 88 |
-
# All Rights Reserved
|
| 89 |
"cnn.com": "All Rights Reserved",
|
| 90 |
"bbc.com": "All Rights Reserved",
|
| 91 |
"bbc.co.uk": "All Rights Reserved",
|
|
@@ -120,141 +120,138 @@ DOMINIOS_EVITADOS: dict[str, str] = {
|
|
| 120 |
"google.es": "All Rights Reserved",
|
| 121 |
}
|
| 122 |
|
| 123 |
-
# โโ
|
| 124 |
|
| 125 |
-
def
|
| 126 |
-
"""
|
| 127 |
url = url.lower().replace("https://", "").replace("http://", "").replace("www.", "")
|
| 128 |
return url.split("/")[0]
|
| 129 |
|
| 130 |
-
def
|
| 131 |
-
"""
|
| 132 |
-
|
| 133 |
-
"""
|
| 134 |
-
dominio = _dominio_de_url(url)
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
return "evitado"
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
return "permitido"
|
| 145 |
|
| 146 |
-
#
|
| 147 |
-
if
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
return "
|
| 151 |
|
| 152 |
-
return "
|
| 153 |
|
| 154 |
-
def
|
| 155 |
-
"""
|
| 156 |
-
|
| 157 |
-
return len(
|
| 158 |
|
| 159 |
-
# โโ
|
| 160 |
|
| 161 |
-
|
| 162 |
-
ERROR_RATE_LIMIT = "error_rate_limit" #
|
| 163 |
-
|
| 164 |
-
|
| 165 |
|
| 166 |
-
def
|
| 167 |
-
"""
|
| 168 |
with DDGS() as ddgs:
|
| 169 |
-
return list(ddgs.text(query, max_results=12, region="
|
| 170 |
|
| 171 |
-
def
|
| 172 |
"""
|
| 173 |
-
|
| 174 |
-
|
| 175 |
"""
|
| 176 |
-
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
for r in
|
| 181 |
url = r.get("href", r.get("url", ""))
|
| 182 |
-
cat =
|
| 183 |
-
if cat == "
|
| 184 |
-
|
| 185 |
-
elif cat == "
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
if not
|
| 191 |
-
return {
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
|
| 202 |
return {
|
| 203 |
-
"
|
| 204 |
-
"url": url, "
|
| 205 |
}
|
| 206 |
|
| 207 |
-
# โโ
|
| 208 |
|
| 209 |
-
def
|
| 210 |
"""
|
| 211 |
-
|
| 212 |
|
| 213 |
-
1.
|
| 214 |
-
2.
|
| 215 |
-
3.
|
| 216 |
-
|
| 217 |
"""
|
| 218 |
-
|
| 219 |
|
| 220 |
-
# โโ 1.
|
| 221 |
-
if
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
return
|
| 225 |
|
| 226 |
-
# โโ 2.
|
| 227 |
-
|
| 228 |
-
|
| 229 |
|
| 230 |
-
for
|
| 231 |
try:
|
| 232 |
-
|
| 233 |
-
|
| 234 |
|
| 235 |
-
|
| 236 |
-
if resultado.get("encontrado"):
|
| 237 |
if len(_CACHE) >= _MAX_CACHE:
|
| 238 |
-
_CACHE.pop(next(iter(_CACHE)))
|
| 239 |
-
_CACHE[
|
| 240 |
|
| 241 |
-
return
|
| 242 |
|
| 243 |
except Exception as e:
|
| 244 |
-
|
| 245 |
-
err_lower
|
| 246 |
|
| 247 |
if "ratelimit" in err_lower or "429" in err_lower or "too many" in err_lower:
|
| 248 |
-
|
| 249 |
-
break
|
| 250 |
|
| 251 |
-
if
|
| 252 |
-
time.sleep(
|
| 253 |
|
| 254 |
-
# โโ 3.
|
| 255 |
return {
|
| 256 |
-
"
|
| 257 |
-
"
|
| 258 |
-
"
|
| 259 |
-
"abstract": "", "snippets": [], "
|
| 260 |
}
|
|
|
|
| 1 |
"""
|
| 2 |
+
buscador.py โ NEO-2 Web search engine with license filtering
|
| 3 |
+
------------------------------------------------------------
|
| 4 |
+
Uses ddgs for real web search. Filters results based on the source
|
| 5 |
+
domain's license to respect content usage terms.
|
| 6 |
+
|
| 7 |
+
NEO-2 license policy:
|
| 8 |
+
โ
ALLOWED : CC BY, CC0, Public Domain
|
| 9 |
+
โ BLOCKED : CC BY-SA, CC BY-NC, CC BY-ND, All Rights Reserved
|
| 10 |
+
|
| 11 |
+
Fault tolerance:
|
| 12 |
+
โข Automatic retries (up to 2 attempts with pause between them)
|
| 13 |
+
โข In-memory session cache (avoids repeated searches)
|
| 14 |
+
โข Typed errors for clear user-facing messages
|
| 15 |
"""
|
| 16 |
|
| 17 |
import time
|
| 18 |
from ddgs import DDGS
|
| 19 |
|
| 20 |
+
# โโ Session cache โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 21 |
+
# Stores results from the current session to avoid re-searching the same query.
|
| 22 |
_CACHE: dict[str, dict] = {}
|
| 23 |
+
_MAX_CACHE = 50
|
| 24 |
|
| 25 |
+
_MAX_RETRIES = 2
|
| 26 |
+
_RETRY_PAUSE = 1.5
|
| 27 |
|
| 28 |
+
# โโ Domain classification by license โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 29 |
|
| 30 |
+
# โ
Allowed: CC BY, CC0 or Public Domain sources
|
| 31 |
+
ALLOWED_DOMAINS: dict[str, str] = {
|
| 32 |
+
# Government / open data (public domain or CC0)
|
| 33 |
+
"nasa.gov": "CC0 / Public Domain",
|
| 34 |
"data.gov": "CC0",
|
| 35 |
+
"datos.gob.mx": "Public Domain",
|
| 36 |
"datos.gob.es": "CC BY",
|
| 37 |
"datos.gob.ar": "CC BY",
|
| 38 |
"open.canada.ca": "CC BY",
|
| 39 |
"data.europa.eu": "CC BY 4.0",
|
| 40 |
"ec.europa.eu": "CC BY 4.0",
|
| 41 |
+
"who.int": "CC BY-NC-SA",
|
| 42 |
+
"un.org": "Public Domain",
|
| 43 |
|
| 44 |
+
# Open knowledge (CC0 / CC BY)
|
| 45 |
"wikidata.org": "CC0",
|
| 46 |
"commons.wikimedia.org": "CC BY / CC0",
|
| 47 |
+
"gutenberg.org": "Public Domain",
|
| 48 |
+
"archive.org": "CC BY / Public Domain",
|
| 49 |
"openstax.org": "CC BY 4.0",
|
| 50 |
+
"arxiv.org": "CC BY / author license",
|
| 51 |
"plos.org": "CC BY 4.0",
|
| 52 |
"doaj.org": "CC BY",
|
| 53 |
"creativecommons.org":"CC BY 4.0",
|
| 54 |
+
"publicdomainreview.org": "CC BY-SA",
|
| 55 |
"freesound.org": "CC0 / CC BY",
|
| 56 |
+
"unsplash.com": "Unsplash License (CC0-like)",
|
| 57 |
"pixabay.com": "CC0",
|
| 58 |
"pexels.com": "CC0",
|
| 59 |
|
| 60 |
+
# Free encyclopedias / dictionaries (CC BY)
|
| 61 |
+
"simple.wikipedia.org": "CC BY-SA",
|
| 62 |
"es.wikiversity.org": "CC BY-SA",
|
| 63 |
}
|
| 64 |
|
| 65 |
+
# โ Blocked: restrictive licenses (CC BY-SA, CC BY-NC, CC BY-ND, ARR)
|
| 66 |
+
BLOCKED_DOMAINS: dict[str, str] = {
|
| 67 |
+
# CC BY-SA (share-alike โ imposes license on derivatives)
|
| 68 |
"wikipedia.org": "CC BY-SA",
|
| 69 |
"wikivoyage.org": "CC BY-SA",
|
| 70 |
"wikibooks.org": "CC BY-SA",
|
|
|
|
| 74 |
"stackexchange.com": "CC BY-SA 4.0",
|
| 75 |
"openstreetmap.org": "ODbL (share-alike)",
|
| 76 |
|
| 77 |
+
# CC BY-NC (non-commercial)
|
| 78 |
"medium.com": "CC BY-NC",
|
| 79 |
"academia.edu": "CC BY-NC",
|
| 80 |
"researchgate.net": "CC BY-NC",
|
| 81 |
"slideshare.net": "CC BY-NC",
|
| 82 |
"ted.com": "CC BY-NC",
|
| 83 |
|
| 84 |
+
# CC BY-ND (no derivatives โ cannot be summarized or paraphrased)
|
| 85 |
"economist.com": "CC BY-ND",
|
| 86 |
"foreignpolicy.com": "CC BY-ND",
|
| 87 |
|
| 88 |
+
# All Rights Reserved
|
| 89 |
"cnn.com": "All Rights Reserved",
|
| 90 |
"bbc.com": "All Rights Reserved",
|
| 91 |
"bbc.co.uk": "All Rights Reserved",
|
|
|
|
| 120 |
"google.es": "All Rights Reserved",
|
| 121 |
}
|
| 122 |
|
| 123 |
+
# โโ Domain helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 124 |
|
| 125 |
+
def _extract_domain(url: str) -> str:
|
| 126 |
+
"""Extracts the base domain from a URL."""
|
| 127 |
url = url.lower().replace("https://", "").replace("http://", "").replace("www.", "")
|
| 128 |
return url.split("/")[0]
|
| 129 |
|
| 130 |
+
def _classify_url(url: str) -> str:
|
| 131 |
+
"""Returns: 'allowed', 'blocked' or 'unknown'."""
|
| 132 |
+
domain = _extract_domain(url)
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
for blocked in BLOCKED_DOMAINS:
|
| 135 |
+
if domain == blocked or domain.endswith("." + blocked):
|
| 136 |
+
return "blocked"
|
|
|
|
| 137 |
|
| 138 |
+
for allowed in ALLOWED_DOMAINS:
|
| 139 |
+
if domain == allowed or domain.endswith("." + allowed):
|
| 140 |
+
return "allowed"
|
|
|
|
| 141 |
|
| 142 |
+
# Heuristic: .gov and .edu domains are usually CC0 / public domain
|
| 143 |
+
if (domain.endswith(".gov") or domain.endswith(".gob") or
|
| 144 |
+
domain.endswith(".gob.mx") or domain.endswith(".gob.ar") or
|
| 145 |
+
domain.endswith(".gob.es") or domain.endswith(".edu")):
|
| 146 |
+
return "allowed"
|
| 147 |
|
| 148 |
+
return "unknown"
|
| 149 |
|
| 150 |
+
def _is_useful_result(result: dict) -> bool:
|
| 151 |
+
"""Discards results that are too short to be useful."""
|
| 152 |
+
body = result.get("body", "").strip()
|
| 153 |
+
return len(body) >= 40
|
| 154 |
|
| 155 |
+
# โโ Error type constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 156 |
|
| 157 |
+
ERROR_NETWORK = "error_network" # connection failure / timeout
|
| 158 |
+
ERROR_RATE_LIMIT = "error_rate_limit" # too many requests
|
| 159 |
+
ERROR_LICENSE = "error_license" # only blocked sources found
|
| 160 |
+
ERROR_NO_DATA = "error_no_data" # search ok but no useful results
|
| 161 |
|
| 162 |
+
def _run_search(query: str) -> list:
|
| 163 |
+
"""Executes the DDGS search. Raises an exception on failure."""
|
| 164 |
with DDGS() as ddgs:
|
| 165 |
+
return list(ddgs.text(query, max_results=12, region="en-us"))
|
| 166 |
|
| 167 |
+
def _process_results(results: list) -> dict:
|
| 168 |
"""
|
| 169 |
+
Filters results by license and builds the response dict.
|
| 170 |
+
Returns the final dict with found=True or found=False.
|
| 171 |
"""
|
| 172 |
+
useful = [r for r in results if _is_useful_result(r)]
|
| 173 |
|
| 174 |
+
allowed = []
|
| 175 |
+
unknown = []
|
| 176 |
+
for r in useful:
|
| 177 |
url = r.get("href", r.get("url", ""))
|
| 178 |
+
cat = _classify_url(url)
|
| 179 |
+
if cat == "allowed":
|
| 180 |
+
allowed.append(r)
|
| 181 |
+
elif cat == "unknown":
|
| 182 |
+
unknown.append(r)
|
| 183 |
+
|
| 184 |
+
candidates = allowed if allowed else unknown
|
| 185 |
+
|
| 186 |
+
if not candidates:
|
| 187 |
+
return {
|
| 188 |
+
"found": False, "error_type": ERROR_LICENSE,
|
| 189 |
+
"abstract": "", "snippets": [], "title": "", "url": "", "license": "",
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
first = candidates[0]
|
| 193 |
+
title = first.get("title", "").strip()
|
| 194 |
+
abstract = first.get("body", "").strip()
|
| 195 |
+
url = first.get("href", first.get("url", "")).strip()
|
| 196 |
+
license_ = ALLOWED_DOMAINS.get(_extract_domain(url), "Unknown")
|
| 197 |
+
snippets = [r.get("body", "").strip() for r in candidates[1:6] if r.get("body", "").strip()]
|
| 198 |
+
origin = "allowed" if allowed else "unknown"
|
| 199 |
|
| 200 |
return {
|
| 201 |
+
"title": title, "abstract": abstract, "snippets": snippets,
|
| 202 |
+
"url": url, "license": license_, "origin": origin, "found": True,
|
| 203 |
}
|
| 204 |
|
| 205 |
+
# โโ Main search function with retries + cache โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 206 |
|
| 207 |
+
def search(query: str) -> dict:
|
| 208 |
"""
|
| 209 |
+
Searches the web with three layers of fault protection:
|
| 210 |
|
| 211 |
+
1. Session cache โ returns a saved result if this query was already searched.
|
| 212 |
+
2. Automatic retries โ tries up to _MAX_RETRIES times with a pause between them.
|
| 213 |
+
3. Typed errors โ distinguishes between network failure, rate-limit and no
|
| 214 |
+
results, so app.py can show clear messages to the user.
|
| 215 |
"""
|
| 216 |
+
cache_key = query.strip().lower()
|
| 217 |
|
| 218 |
+
# โโ 1. Cache โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 219 |
+
if cache_key in _CACHE:
|
| 220 |
+
result = dict(_CACHE[cache_key])
|
| 221 |
+
result["from_cache"] = True
|
| 222 |
+
return result
|
| 223 |
|
| 224 |
+
# โโ 2. Retries โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 225 |
+
last_error = None
|
| 226 |
+
error_type = ERROR_NETWORK
|
| 227 |
|
| 228 |
+
for attempt in range(_MAX_RETRIES):
|
| 229 |
try:
|
| 230 |
+
raw_results = _run_search(query)
|
| 231 |
+
result = _process_results(raw_results)
|
| 232 |
|
| 233 |
+
if result.get("found"):
|
|
|
|
| 234 |
if len(_CACHE) >= _MAX_CACHE:
|
| 235 |
+
_CACHE.pop(next(iter(_CACHE)))
|
| 236 |
+
_CACHE[cache_key] = result
|
| 237 |
|
| 238 |
+
return result
|
| 239 |
|
| 240 |
except Exception as e:
|
| 241 |
+
last_error = str(e)
|
| 242 |
+
err_lower = last_error.lower()
|
| 243 |
|
| 244 |
if "ratelimit" in err_lower or "429" in err_lower or "too many" in err_lower:
|
| 245 |
+
error_type = ERROR_RATE_LIMIT
|
| 246 |
+
break
|
| 247 |
|
| 248 |
+
if attempt < _MAX_RETRIES - 1:
|
| 249 |
+
time.sleep(_RETRY_PAUSE)
|
| 250 |
|
| 251 |
+
# โโ 3. Definitive failure โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 252 |
return {
|
| 253 |
+
"found": False,
|
| 254 |
+
"error_type": error_type,
|
| 255 |
+
"detail": last_error or "Unknown error",
|
| 256 |
+
"abstract": "", "snippets": [], "title": "", "url": "", "license": "",
|
| 257 |
}
|
chat-app/compilar_respuestas.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
| 1 |
"""
|
| 2 |
-
compilar_respuestas.py โ NEO-1 Closed Edition
|
| 3 |
-
----------------------------------------------
|
| 4 |
-
|
| 5 |
|
| 6 |
-
|
| 7 |
python chat-app/compilar_respuestas.py
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
| 14 |
"""
|
| 15 |
|
| 16 |
import os
|
|
@@ -19,74 +22,73 @@ import zlib
|
|
| 19 |
import base64
|
| 20 |
import json
|
| 21 |
|
| 22 |
-
_DIR
|
| 23 |
-
|
| 24 |
-
|
| 25 |
|
| 26 |
-
def
|
| 27 |
key = os.environ.get("NEO_DATA_KEY", "neo1-mdfj-science-closed-2025")
|
| 28 |
return key.encode("utf-8")
|
| 29 |
|
| 30 |
def _xor(data: bytes, key: bytes) -> bytes:
|
| 31 |
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
| 32 |
|
| 33 |
-
def
|
| 34 |
-
if not os.path.exists(
|
| 35 |
-
print(f"โ
|
| 36 |
sys.exit(1)
|
| 37 |
|
| 38 |
-
with open(
|
| 39 |
-
|
| 40 |
|
| 41 |
-
# Validar JSON antes de compilar
|
| 42 |
try:
|
| 43 |
-
|
| 44 |
-
|
| 45 |
except json.JSONDecodeError as e:
|
| 46 |
-
print(f"โ
|
| 47 |
sys.exit(1)
|
| 48 |
|
| 49 |
-
raw
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
encoded = base64.b64encode(
|
| 53 |
|
| 54 |
-
with open(
|
| 55 |
f.write(encoded)
|
| 56 |
|
| 57 |
-
print(
|
| 58 |
-
print(f"
|
| 59 |
-
print(f"
|
| 60 |
-
print(f"
|
| 61 |
-
print(f"
|
| 62 |
print()
|
| 63 |
-
print("๐ฆ
|
| 64 |
-
print(" -
|
| 65 |
-
print(" -
|
| 66 |
-
print(" -
|
| 67 |
-
|
| 68 |
-
def
|
| 69 |
-
"""
|
| 70 |
-
if not os.path.exists(
|
| 71 |
-
print(f"โ
|
| 72 |
sys.exit(1)
|
| 73 |
|
| 74 |
-
with open(
|
| 75 |
encoded = f.read()
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
raw = zlib.decompress(
|
| 80 |
-
|
| 81 |
|
| 82 |
-
|
| 83 |
-
with open(
|
| 84 |
-
f.write(
|
| 85 |
|
| 86 |
-
print(f"โ
|
| 87 |
|
| 88 |
if __name__ == "__main__":
|
| 89 |
-
if len(sys.argv) > 1 and sys.argv[1] == "--
|
| 90 |
-
|
| 91 |
else:
|
| 92 |
-
|
|
|
|
| 1 |
"""
|
| 2 |
+
compilar_respuestas.py โ NEO-1 Closed Edition compiler
|
| 3 |
+
-------------------------------------------------------
|
| 4 |
+
Converts respuestas.json into respuestas.dat (encrypted + compressed).
|
| 5 |
|
| 6 |
+
Usage:
|
| 7 |
python chat-app/compilar_respuestas.py
|
| 8 |
|
| 9 |
+
The resulting .dat file is the closed edition of the knowledge base.
|
| 10 |
+
Include this .dat in your private repository and exclude respuestas.json.
|
| 11 |
|
| 12 |
+
The encryption key is read from the NEO_DATA_KEY environment variable.
|
| 13 |
+
If not set, falls back to the internal default key.
|
| 14 |
+
|
| 15 |
+
Decompile (owner only):
|
| 16 |
+
python chat-app/compilar_respuestas.py --decompile [output_path]
|
| 17 |
"""
|
| 18 |
|
| 19 |
import os
|
|
|
|
| 22 |
import base64
|
| 23 |
import json
|
| 24 |
|
| 25 |
+
_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 26 |
+
JSON_PATH = os.path.join(_DIR, "respuestas.json")
|
| 27 |
+
DAT_PATH = os.path.join(_DIR, "respuestas.dat")
|
| 28 |
|
| 29 |
+
def _get_key() -> bytes:
|
| 30 |
key = os.environ.get("NEO_DATA_KEY", "neo1-mdfj-science-closed-2025")
|
| 31 |
return key.encode("utf-8")
|
| 32 |
|
| 33 |
def _xor(data: bytes, key: bytes) -> bytes:
|
| 34 |
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
| 35 |
|
| 36 |
+
def compile_knowledge():
|
| 37 |
+
if not os.path.exists(JSON_PATH):
|
| 38 |
+
print(f"โ File not found: {JSON_PATH}")
|
| 39 |
sys.exit(1)
|
| 40 |
|
| 41 |
+
with open(JSON_PATH, "r", encoding="utf-8") as f:
|
| 42 |
+
content = f.read()
|
| 43 |
|
|
|
|
| 44 |
try:
|
| 45 |
+
data = json.loads(content)
|
| 46 |
+
entry_count = len(data.get("respuestas", []))
|
| 47 |
except json.JSONDecodeError as e:
|
| 48 |
+
print(f"โ Invalid JSON: {e}")
|
| 49 |
sys.exit(1)
|
| 50 |
|
| 51 |
+
raw = content.encode("utf-8")
|
| 52 |
+
compressed = zlib.compress(raw, level=9)
|
| 53 |
+
encrypted = _xor(compressed, _get_key())
|
| 54 |
+
encoded = base64.b64encode(encrypted)
|
| 55 |
|
| 56 |
+
with open(DAT_PATH, "wb") as f:
|
| 57 |
f.write(encoded)
|
| 58 |
|
| 59 |
+
print("โ
Compiled successfully:")
|
| 60 |
+
print(f" Knowledge entries : {entry_count}")
|
| 61 |
+
print(f" Original JSON : {len(raw):,} bytes")
|
| 62 |
+
print(f" Encrypted DAT : {len(encoded):,} bytes")
|
| 63 |
+
print(f" Saved to : {DAT_PATH}")
|
| 64 |
print()
|
| 65 |
+
print("๐ฆ For the closed edition:")
|
| 66 |
+
print(" - Include 'respuestas.dat' in your private repo")
|
| 67 |
+
print(" - Do NOT include 'respuestas.json'")
|
| 68 |
+
print(" - Set NEO_DATA_KEY as a secret environment variable")
|
| 69 |
+
|
| 70 |
+
def decompile_knowledge(output_path: str | None = None):
|
| 71 |
+
"""Utility: converts respuestas.dat back to JSON (owner only)."""
|
| 72 |
+
if not os.path.exists(DAT_PATH):
|
| 73 |
+
print(f"โ File not found: {DAT_PATH}")
|
| 74 |
sys.exit(1)
|
| 75 |
|
| 76 |
+
with open(DAT_PATH, "rb") as f:
|
| 77 |
encoded = f.read()
|
| 78 |
|
| 79 |
+
encrypted = base64.b64decode(encoded)
|
| 80 |
+
compressed = _xor(encrypted, _get_key())
|
| 81 |
+
raw = zlib.decompress(compressed)
|
| 82 |
+
content = raw.decode("utf-8")
|
| 83 |
|
| 84 |
+
destination = output_path or JSON_PATH.replace(".json", "_decompiled.json")
|
| 85 |
+
with open(destination, "w", encoding="utf-8") as f:
|
| 86 |
+
f.write(content)
|
| 87 |
|
| 88 |
+
print(f"โ
Decompiled to: {destination}")
|
| 89 |
|
| 90 |
if __name__ == "__main__":
|
| 91 |
+
if len(sys.argv) > 1 and sys.argv[1] == "--decompile":
|
| 92 |
+
decompile_knowledge(sys.argv[2] if len(sys.argv) > 2 else None)
|
| 93 |
else:
|
| 94 |
+
compile_knowledge()
|
chat-app/logica.py
CHANGED
|
@@ -5,166 +5,161 @@ import zlib
|
|
| 5 |
import base64
|
| 6 |
import random
|
| 7 |
import unicodedata
|
| 8 |
-
from roblox_api import
|
| 9 |
from matematicas import (
|
| 10 |
-
|
| 11 |
-
|
| 12 |
)
|
| 13 |
-
from buscador import
|
| 14 |
-
from resumidor import
|
| 15 |
|
| 16 |
-
# โโ
|
| 17 |
|
| 18 |
INTROS = [
|
| 19 |
"",
|
| 20 |
-
"
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
]
|
| 28 |
|
| 29 |
-
|
| 30 |
-
"
|
| 31 |
-
"
|
| 32 |
-
"
|
| 33 |
-
"
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
]
|
| 37 |
|
| 38 |
-
|
| 39 |
"",
|
| 40 |
-
"\n\
|
| 41 |
-
"\n\
|
| 42 |
-
"\n\
|
| 43 |
-
"\n\
|
| 44 |
-
"\n\
|
| 45 |
]
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
|
| 49 |
-
"
|
| 50 |
-
"
|
| 51 |
)
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
"""
|
| 55 |
-
bl =
|
| 56 |
-
return any(
|
| 57 |
|
| 58 |
-
def
|
| 59 |
"""
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
1.
|
| 63 |
-
2.
|
| 64 |
-
3.
|
| 65 |
-
4.
|
| 66 |
-
5.
|
| 67 |
"""
|
| 68 |
-
|
| 69 |
|
| 70 |
-
if len(
|
| 71 |
-
intro
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
return texto.strip()
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
medio = bloques[1:-1]
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
fijos = [b for b in medio if _es_bloque_fijo(b)]
|
| 84 |
|
| 85 |
random.shuffle(shuffleable)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
"
|
| 104 |
-
"
|
| 105 |
-
"
|
| 106 |
-
"
|
| 107 |
-
"
|
| 108 |
-
"
|
| 109 |
-
"
|
| 110 |
-
"
|
| 111 |
-
"
|
| 112 |
-
"
|
| 113 |
-
"
|
| 114 |
-
"
|
| 115 |
-
"
|
| 116 |
-
"
|
| 117 |
-
"
|
| 118 |
-
"
|
| 119 |
-
"Tycoon": "un juego de gestiรณn y negocios (tycoon)",
|
| 120 |
-
"Obby": "un juego de obstรกculos (obby)",
|
| 121 |
-
"All Genres": "un juego variado",
|
| 122 |
}
|
| 123 |
|
| 124 |
def _xor(data: bytes, key: bytes) -> bytes:
|
| 125 |
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
| 126 |
|
| 127 |
-
def
|
| 128 |
key = os.environ.get("NEO_DATA_KEY", "neo1-mdfj-science-closed-2025")
|
| 129 |
return key.encode("utf-8")
|
| 130 |
|
| 131 |
-
def
|
| 132 |
"""
|
| 133 |
-
|
| 134 |
-
1.
|
| 135 |
-
2.
|
|
|
|
| 136 |
"""
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
|
| 141 |
-
# โโ
|
| 142 |
-
if os.path.exists(
|
| 143 |
try:
|
| 144 |
-
with open(
|
| 145 |
encoded = f.read()
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
raw
|
| 149 |
-
|
| 150 |
-
return
|
| 151 |
except Exception:
|
| 152 |
-
pass
|
| 153 |
|
| 154 |
-
# โโ
|
| 155 |
try:
|
| 156 |
-
with open(
|
| 157 |
-
|
| 158 |
-
return
|
| 159 |
except Exception:
|
| 160 |
return []
|
| 161 |
|
| 162 |
-
|
| 163 |
|
| 164 |
-
# โโ
|
| 165 |
|
| 166 |
-
#
|
| 167 |
-
#
|
| 168 |
STOPWORDS = {
|
| 169 |
"que", "es", "el", "la", "los", "las", "un", "una", "unos", "unas",
|
| 170 |
"de", "del", "al", "a", "en", "por", "para", "con", "sin", "sobre",
|
|
@@ -175,210 +170,229 @@ STOPWORDS = {
|
|
| 175 |
"como", "cรณmo", "dรณnde", "quiรฉn", "quรฉ", "cuรกnto", "cuanto",
|
| 176 |
"este", "esta", "estos", "estas", "ese", "esa", "esos", "esas",
|
| 177 |
"me", "puedes", "puedo", "puede", "quiero", "quieres", "dame",
|
| 178 |
-
"dime", "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
}
|
| 180 |
|
| 181 |
-
def
|
| 182 |
"""
|
| 183 |
-
|
| 184 |
-
1.
|
| 185 |
-
2.
|
| 186 |
-
3.
|
| 187 |
-
4.
|
| 188 |
"""
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
return [t for t in
|
| 194 |
|
| 195 |
-
def
|
| 196 |
"""
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
"""
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
-
if not
|
| 205 |
return 0.0
|
| 206 |
|
| 207 |
-
|
| 208 |
|
| 209 |
-
|
| 210 |
-
if not interseccion:
|
| 211 |
return 0.0
|
| 212 |
|
| 213 |
-
union =
|
| 214 |
-
jaccard = len(
|
| 215 |
|
| 216 |
-
# Bonus:
|
| 217 |
-
if
|
| 218 |
jaccard = max(jaccard, 0.80)
|
| 219 |
|
| 220 |
return jaccard
|
| 221 |
|
| 222 |
-
def
|
| 223 |
"""
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
"""
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
for
|
| 235 |
-
for
|
| 236 |
-
|
| 237 |
-
score =
|
| 238 |
-
if score >
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
if
|
| 243 |
-
return
|
| 244 |
return None
|
| 245 |
|
| 246 |
-
# โโ
|
| 247 |
|
| 248 |
-
def
|
| 249 |
if not content:
|
| 250 |
return ""
|
| 251 |
if isinstance(content, str):
|
| 252 |
return content
|
| 253 |
if isinstance(content, list):
|
| 254 |
-
|
| 255 |
-
for
|
| 256 |
-
if isinstance(
|
| 257 |
-
|
| 258 |
-
elif isinstance(
|
| 259 |
-
|
| 260 |
-
return " ".join(
|
| 261 |
return str(content)
|
| 262 |
|
| 263 |
-
def
|
| 264 |
-
if not
|
| 265 |
return False
|
| 266 |
-
for msg in reversed(
|
| 267 |
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 268 |
-
|
| 269 |
-
return "
|
| 270 |
return False
|
| 271 |
|
| 272 |
-
# โโ ROBLOX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 273 |
|
| 274 |
-
def
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
|
| 284 |
-
if
|
| 285 |
-
|
| 286 |
-
|
| 287 |
|
| 288 |
try:
|
| 289 |
-
v = int(
|
| 290 |
if v >= 1_000_000_000:
|
| 291 |
-
|
| 292 |
elif v >= 1_000_000:
|
| 293 |
-
|
| 294 |
elif v >= 1_000:
|
| 295 |
-
|
| 296 |
except Exception:
|
| 297 |
pass
|
| 298 |
|
| 299 |
-
return " ".join(
|
| 300 |
-
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
r"buscar\s+jugador\s+(.+)",
|
| 303 |
r"busca\s+jugador\s+(.+)",
|
| 304 |
r"jugador\s+de\s+roblox\s+(.+)",
|
| 305 |
r"usuario\s+de\s+roblox\s+(.+)",
|
| 306 |
r"perfil\s+de\s+roblox\s+(.+)",
|
| 307 |
-
r"buscar\s+usuario\s+(.+)",
|
| 308 |
-
r"busca\s+usuario\s+(.+)",
|
| 309 |
r"quien\s+es\s+(.+)\s+en\s+roblox",
|
| 310 |
-
r"quiรฉn\s+es\s+(.+)\s+en\s+roblox",
|
| 311 |
-
r"info\s+de\s+(.+)\s+roblox",
|
| 312 |
]
|
| 313 |
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
r"buscar\s+juego\s+(.+)",
|
| 316 |
r"busca\s+juego\s+(.+)",
|
| 317 |
r"juego\s+de\s+roblox\s+(.+)",
|
| 318 |
r"buscar\s+(.+)\s+en\s+roblox",
|
| 319 |
-
r"busca\s+(.+)\s+en\s+roblox",
|
| 320 |
r"informaciรณn\s+del\s+juego\s+(.+)",
|
| 321 |
-
r"informacion\s+del\s+juego\s+(.+)",
|
| 322 |
]
|
| 323 |
|
| 324 |
-
def
|
| 325 |
-
|
| 326 |
-
for
|
| 327 |
-
m = re.search(
|
| 328 |
if m:
|
| 329 |
-
return "
|
| 330 |
-
for
|
| 331 |
-
m = re.search(
|
| 332 |
if m:
|
| 333 |
-
return "
|
| 334 |
return None, None
|
| 335 |
|
| 336 |
-
# โโ
|
| 337 |
|
| 338 |
-
def
|
| 339 |
-
|
| 340 |
|
| 341 |
-
if
|
| 342 |
-
|
| 343 |
return (
|
| 344 |
-
f"
|
| 345 |
-
"๐งฎ **
|
| 346 |
-
"
|
| 347 |
-
"**
|
| 348 |
-
"
|
| 349 |
)
|
| 350 |
|
| 351 |
-
if
|
| 352 |
-
return "
|
| 353 |
|
| 354 |
-
if
|
| 355 |
-
|
| 356 |
-
if
|
| 357 |
-
return
|
| 358 |
|
| 359 |
-
|
| 360 |
|
| 361 |
-
if
|
| 362 |
-
|
| 363 |
-
return
|
| 364 |
|
| 365 |
-
if
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
if
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
return
|
| 372 |
|
| 373 |
-
|
| 374 |
-
if
|
| 375 |
-
return
|
| 376 |
|
| 377 |
-
# โโ NEO-2:
|
| 378 |
-
|
| 379 |
-
if
|
| 380 |
-
|
| 381 |
-
if
|
| 382 |
-
return
|
| 383 |
|
| 384 |
-
return "๐ค
|
|
|
|
| 5 |
import base64
|
| 6 |
import random
|
| 7 |
import unicodedata
|
| 8 |
+
from roblox_api import search_player, search_game, format_player, format_game
|
| 9 |
from matematicas import (
|
| 10 |
+
is_calculator_request, is_math_operation,
|
| 11 |
+
solve_operation, format_result, extract_username,
|
| 12 |
)
|
| 13 |
+
from buscador import search as web_search
|
| 14 |
+
from resumidor import summarize
|
| 15 |
|
| 16 |
+
# โโ LANGUAGE VARIATION BANK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 17 |
|
| 18 |
INTROS = [
|
| 19 |
"",
|
| 20 |
+
"Of course! ",
|
| 21 |
+
"Gladly! ",
|
| 22 |
+
"Great question! ",
|
| 23 |
+
"Here's what I know: ",
|
| 24 |
+
"Certainly. ",
|
| 25 |
+
"Here we go: ",
|
| 26 |
+
"Let me explain. ",
|
| 27 |
]
|
| 28 |
|
| 29 |
+
CONNECTORS = [
|
| 30 |
+
"Also, ",
|
| 31 |
+
"Worth mentioning that ",
|
| 32 |
+
"On the other hand, ",
|
| 33 |
+
"It's worth adding that ",
|
| 34 |
+
"Something important: ",
|
| 35 |
+
"Don't forget that ",
|
| 36 |
]
|
| 37 |
|
| 38 |
+
CLOSINGS = [
|
| 39 |
"",
|
| 40 |
+
"\n\nDo you have any other questions? ๐",
|
| 41 |
+
"\n\nWould you like me to go deeper on anything? ๐ค",
|
| 42 |
+
"\n\nNeed more details on this?",
|
| 43 |
+
"\n\nAnything else I can help you with?",
|
| 44 |
+
"\n\nHope that was helpful! ๐",
|
| 45 |
]
|
| 46 |
|
| 47 |
+
# Words that indicate a block is an intro/closing and should not be shuffled
|
| 48 |
+
_FIXED_MARKERS = (
|
| 49 |
+
"here's what", "here we go", "let me", "i'm ", "hello", "of course",
|
| 50 |
+
"alright", "my pleasure", "gladly", "certainly",
|
| 51 |
)
|
| 52 |
|
| 53 |
+
def _is_fixed_block(block):
|
| 54 |
+
"""Returns True if the block should stay in its original position."""
|
| 55 |
+
bl = block.lower()
|
| 56 |
+
return any(marker in bl for marker in _FIXED_MARKERS)
|
| 57 |
|
| 58 |
+
def generate_variation(response):
|
| 59 |
"""
|
| 60 |
+
Generates a varied version of the response so NEO-1 doesn't sound
|
| 61 |
+
repetitive. Strategy:
|
| 62 |
+
1. Split the response into paragraphs/blocks (separated by \\n\\n).
|
| 63 |
+
2. Keep the first block fixed if it looks like an intro.
|
| 64 |
+
3. Randomly shuffle the middle blocks.
|
| 65 |
+
4. Optionally append a different closing.
|
| 66 |
+
5. With some probability, insert a connector before a block.
|
| 67 |
"""
|
| 68 |
+
blocks = [b.strip() for b in response.split("\n\n") if b.strip()]
|
| 69 |
|
| 70 |
+
if len(blocks) <= 2:
|
| 71 |
+
intro = random.choice(INTROS[:4])
|
| 72 |
+
closing = random.choice(CLOSINGS)
|
| 73 |
+
return (intro + response + closing).strip()
|
|
|
|
| 74 |
|
| 75 |
+
start = blocks[0]
|
| 76 |
+
end = blocks[-1]
|
| 77 |
+
middle = blocks[1:-1]
|
|
|
|
| 78 |
|
| 79 |
+
shuffleable = [b for b in middle if not _is_fixed_block(b)]
|
| 80 |
+
fixed = [b for b in middle if _is_fixed_block(b)]
|
|
|
|
| 81 |
|
| 82 |
random.shuffle(shuffleable)
|
| 83 |
+
new_middle = fixed + shuffleable
|
| 84 |
+
|
| 85 |
+
if len(new_middle) > 1 and random.random() < 0.40:
|
| 86 |
+
idx = random.randint(1, len(new_middle) - 1)
|
| 87 |
+
connector = random.choice(CONNECTORS)
|
| 88 |
+
if not new_middle[idx].startswith(("**", "๐", "๐", "โ๏ธ", "๐ง", "๐ข", "๐ฌ๏ธ")):
|
| 89 |
+
new_middle[idx] = connector + new_middle[idx]
|
| 90 |
+
|
| 91 |
+
closing = random.choice(CLOSINGS) if random.random() < 0.50 else ""
|
| 92 |
+
|
| 93 |
+
parts = [start] + new_middle + [end]
|
| 94 |
+
return "\n\n".join(parts) + closing
|
| 95 |
+
|
| 96 |
+
GAME_GENRES = {
|
| 97 |
+
"Town and City": "an urban life and social roleplay game",
|
| 98 |
+
"Adventure": "an adventure and exploration game",
|
| 99 |
+
"Role Playing": "a role-playing game where you can be anyone",
|
| 100 |
+
"Comedy": "a comedy and entertainment game",
|
| 101 |
+
"Action": "an action and combat game",
|
| 102 |
+
"Horror": "a horror and suspense game",
|
| 103 |
+
"Military": "a military-themed strategy game",
|
| 104 |
+
"Medieval": "a medieval-themed game with castles and knights",
|
| 105 |
+
"Naval": "a naval adventure and pirates game",
|
| 106 |
+
"Sci-Fi": "a science fiction game",
|
| 107 |
+
"Sports": "a sports game",
|
| 108 |
+
"Fighting": "a fighting and combat game",
|
| 109 |
+
"Western": "a Wild West-themed game",
|
| 110 |
+
"FPS": "a first-person shooter",
|
| 111 |
+
"Building": "a building and creativity game",
|
| 112 |
+
"Simulator": "a simulator",
|
| 113 |
+
"Tycoon": "a management and business tycoon game",
|
| 114 |
+
"Obby": "an obstacle course game (obby)",
|
| 115 |
+
"All Genres": "a mixed-genre game",
|
|
|
|
|
|
|
|
|
|
| 116 |
}
|
| 117 |
|
| 118 |
def _xor(data: bytes, key: bytes) -> bytes:
|
| 119 |
return bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
|
| 120 |
|
| 121 |
+
def _get_key() -> bytes:
|
| 122 |
key = os.environ.get("NEO_DATA_KEY", "neo1-mdfj-science-closed-2025")
|
| 123 |
return key.encode("utf-8")
|
| 124 |
|
| 125 |
+
def load_responses():
|
| 126 |
"""
|
| 127 |
+
Loads the knowledge base. Priority order:
|
| 128 |
+
1. responses.dat โ closed edition (encrypted + compressed)
|
| 129 |
+
2. responses.json โ open edition (plain text)
|
| 130 |
+
Falls back silently if both fail.
|
| 131 |
"""
|
| 132 |
+
directory = os.path.dirname(__file__)
|
| 133 |
+
dat_path = os.path.join(directory, "respuestas.dat")
|
| 134 |
+
json_path = os.path.join(directory, "respuestas.json")
|
| 135 |
|
| 136 |
+
# โโ Closed edition (.dat) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 137 |
+
if os.path.exists(dat_path):
|
| 138 |
try:
|
| 139 |
+
with open(dat_path, "rb") as f:
|
| 140 |
encoded = f.read()
|
| 141 |
+
encrypted = base64.b64decode(encoded)
|
| 142 |
+
compressed = _xor(encrypted, _get_key())
|
| 143 |
+
raw = zlib.decompress(compressed)
|
| 144 |
+
data = json.loads(raw.decode("utf-8"))
|
| 145 |
+
return data.get("respuestas", [])
|
| 146 |
except Exception:
|
| 147 |
+
pass
|
| 148 |
|
| 149 |
+
# โโ Open edition (.json) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 150 |
try:
|
| 151 |
+
with open(json_path, "r", encoding="utf-8") as f:
|
| 152 |
+
data = json.load(f)
|
| 153 |
+
return data.get("respuestas", [])
|
| 154 |
except Exception:
|
| 155 |
return []
|
| 156 |
|
| 157 |
+
KNOWLEDGE_BASE = load_responses()
|
| 158 |
|
| 159 |
+
# โโ TOKENIZER โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 160 |
|
| 161 |
+
# Spanish stopwords โ they carry no semantic meaning on their own.
|
| 162 |
+
# Filtering them prevents false matches like "what is a supernova" โ licenses.
|
| 163 |
STOPWORDS = {
|
| 164 |
"que", "es", "el", "la", "los", "las", "un", "una", "unos", "unas",
|
| 165 |
"de", "del", "al", "a", "en", "por", "para", "con", "sin", "sobre",
|
|
|
|
| 170 |
"como", "cรณmo", "dรณnde", "quiรฉn", "quรฉ", "cuรกnto", "cuanto",
|
| 171 |
"este", "esta", "estos", "estas", "ese", "esa", "esos", "esas",
|
| 172 |
"me", "puedes", "puedo", "puede", "quiero", "quieres", "dame",
|
| 173 |
+
"dime", "hay", "tiene", "tienen", "tengo",
|
| 174 |
+
# English stopwords
|
| 175 |
+
"the", "is", "are", "was", "were", "be", "been", "being",
|
| 176 |
+
"a", "an", "and", "or", "but", "if", "in", "on", "at", "to",
|
| 177 |
+
"for", "of", "with", "by", "from", "as", "what", "who", "how",
|
| 178 |
+
"when", "where", "which", "that", "this", "do", "does", "did",
|
| 179 |
+
"can", "could", "will", "would", "should", "may", "might",
|
| 180 |
+
"i", "you", "he", "she", "it", "we", "they", "me", "him", "her",
|
| 181 |
+
"us", "them", "my", "your", "his", "its", "our", "their",
|
| 182 |
}
|
| 183 |
|
| 184 |
+
def tokenize(text):
|
| 185 |
"""
|
| 186 |
+
Converts text to a list of normalized tokens:
|
| 187 |
+
1. Removes accents/diacritics
|
| 188 |
+
2. Lowercases
|
| 189 |
+
3. Removes punctuation
|
| 190 |
+
4. Splits into tokens and filters stopwords
|
| 191 |
"""
|
| 192 |
+
text = unicodedata.normalize("NFD", text)
|
| 193 |
+
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
|
| 194 |
+
text = text.lower()
|
| 195 |
+
text = re.sub(r"[^\w\s]", " ", text)
|
| 196 |
+
return [t for t in text.split() if t and t not in STOPWORDS]
|
| 197 |
|
| 198 |
+
def token_similarity(input_tokens, pattern_tokens):
|
| 199 |
"""
|
| 200 |
+
Computes Jaccard similarity over significant tokens (without stopwords).
|
| 201 |
+
Minimum requirement: at least 1 significant token in common.
|
| 202 |
+
Returns 0.0 if there is no significant token overlap.
|
| 203 |
"""
|
| 204 |
+
set_input = set(input_tokens)
|
| 205 |
+
set_pattern = set(pattern_tokens)
|
| 206 |
|
| 207 |
+
if not set_pattern or not set_input:
|
| 208 |
return 0.0
|
| 209 |
|
| 210 |
+
intersection = set_input & set_pattern
|
| 211 |
|
| 212 |
+
if not intersection:
|
|
|
|
| 213 |
return 0.0
|
| 214 |
|
| 215 |
+
union = set_input | set_pattern
|
| 216 |
+
jaccard = len(intersection) / len(union)
|
| 217 |
|
| 218 |
+
# Bonus: if all pattern tokens are present in the input
|
| 219 |
+
if set_pattern.issubset(set_input):
|
| 220 |
jaccard = max(jaccard, 0.80)
|
| 221 |
|
| 222 |
return jaccard
|
| 223 |
|
| 224 |
+
def find_custom_response(message):
|
| 225 |
"""
|
| 226 |
+
Finds the best matching response using token similarity and returns
|
| 227 |
+
an automatically generated variation so NEO-1 doesn't repeat
|
| 228 |
+
the exact same words every time.
|
| 229 |
+
Minimum threshold: 0.20 (at least 20% Jaccard overlap)
|
| 230 |
"""
|
| 231 |
+
input_tokens = tokenize(message)
|
| 232 |
+
best_response = None
|
| 233 |
+
best_score = 0.0
|
| 234 |
+
THRESHOLD = 0.20
|
| 235 |
+
|
| 236 |
+
for entry in KNOWLEDGE_BASE:
|
| 237 |
+
for question in entry.get("preguntas", []):
|
| 238 |
+
pattern_tokens = tokenize(question)
|
| 239 |
+
score = token_similarity(input_tokens, pattern_tokens)
|
| 240 |
+
if score > best_score:
|
| 241 |
+
best_score = score
|
| 242 |
+
best_response = entry.get("respuesta")
|
| 243 |
+
|
| 244 |
+
if best_score >= THRESHOLD and best_response:
|
| 245 |
+
return generate_variation(best_response)
|
| 246 |
return None
|
| 247 |
|
| 248 |
+
# โโ UTILITIES โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 249 |
|
| 250 |
+
def extract_text_content(content):
|
| 251 |
if not content:
|
| 252 |
return ""
|
| 253 |
if isinstance(content, str):
|
| 254 |
return content
|
| 255 |
if isinstance(content, list):
|
| 256 |
+
parts = []
|
| 257 |
+
for block in content:
|
| 258 |
+
if isinstance(block, str):
|
| 259 |
+
parts.append(block)
|
| 260 |
+
elif isinstance(block, dict):
|
| 261 |
+
parts.append(str(block.get("text") or block.get("value") or block.get("content") or ""))
|
| 262 |
+
return " ".join(parts)
|
| 263 |
return str(content)
|
| 264 |
|
| 265 |
+
def calculator_mode_active(history):
|
| 266 |
+
if not history:
|
| 267 |
return False
|
| 268 |
+
for msg in reversed(history):
|
| 269 |
if isinstance(msg, dict) and msg.get("role") == "assistant":
|
| 270 |
+
text = extract_text_content(msg.get("content")).lower()
|
| 271 |
+
return "neo-1 virtual calculator" in text or "here's our calculator" in text
|
| 272 |
return False
|
| 273 |
|
| 274 |
+
# โโ ROBLOX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 275 |
|
| 276 |
+
def generate_game_explanation(data):
|
| 277 |
+
name = data.get("name", "This game")
|
| 278 |
+
genre = data.get("genre", "")
|
| 279 |
+
description = data.get("description", "").strip()
|
| 280 |
+
visits = data.get("visits", 0)
|
| 281 |
+
creator = data.get("creator", "")
|
| 282 |
|
| 283 |
+
genre_desc = GAME_GENRES.get(genre, f"a {genre.lower()} game" if genre else "a game")
|
| 284 |
+
parts = [f"**{name}** is {genre_desc} on Roblox created by **{creator}**."]
|
| 285 |
|
| 286 |
+
if description and description != "No description.":
|
| 287 |
+
short_desc = description[:200] + ("..." if len(description) > 200 else "")
|
| 288 |
+
parts.append(f"According to its official description: *\"{short_desc}\"*")
|
| 289 |
|
| 290 |
try:
|
| 291 |
+
v = int(visits)
|
| 292 |
if v >= 1_000_000_000:
|
| 293 |
+
parts.append(f"It's one of the most visited games on Roblox with over {v // 1_000_000_000}B visits.")
|
| 294 |
elif v >= 1_000_000:
|
| 295 |
+
parts.append(f"It has over {v // 1_000_000}M total visits.")
|
| 296 |
elif v >= 1_000:
|
| 297 |
+
parts.append(f"It has over {v // 1_000}K total visits.")
|
| 298 |
except Exception:
|
| 299 |
pass
|
| 300 |
|
| 301 |
+
return " ".join(parts)
|
| 302 |
+
|
| 303 |
+
PLAYER_PATTERNS = [
|
| 304 |
+
r"search\s+player\s+(.+)",
|
| 305 |
+
r"find\s+player\s+(.+)",
|
| 306 |
+
r"roblox\s+player\s+(.+)",
|
| 307 |
+
r"roblox\s+user\s+(.+)",
|
| 308 |
+
r"roblox\s+profile\s+(.+)",
|
| 309 |
+
r"search\s+user\s+(.+)",
|
| 310 |
+
r"find\s+user\s+(.+)",
|
| 311 |
+
r"who\s+is\s+(.+)\s+on\s+roblox",
|
| 312 |
+
r"info\s+(?:on|about)\s+(.+)\s+roblox",
|
| 313 |
+
# Spanish patterns (kept for backward compatibility)
|
| 314 |
r"buscar\s+jugador\s+(.+)",
|
| 315 |
r"busca\s+jugador\s+(.+)",
|
| 316 |
r"jugador\s+de\s+roblox\s+(.+)",
|
| 317 |
r"usuario\s+de\s+roblox\s+(.+)",
|
| 318 |
r"perfil\s+de\s+roblox\s+(.+)",
|
|
|
|
|
|
|
| 319 |
r"quien\s+es\s+(.+)\s+en\s+roblox",
|
|
|
|
|
|
|
| 320 |
]
|
| 321 |
|
| 322 |
+
GAME_PATTERNS = [
|
| 323 |
+
r"search\s+game\s+(.+)",
|
| 324 |
+
r"find\s+game\s+(.+)",
|
| 325 |
+
r"roblox\s+game\s+(.+)",
|
| 326 |
+
r"search\s+(.+)\s+on\s+roblox",
|
| 327 |
+
r"find\s+(.+)\s+on\s+roblox",
|
| 328 |
+
r"game\s+info\s+(.+)",
|
| 329 |
+
r"game\s+information\s+(.+)",
|
| 330 |
+
# Spanish patterns (kept for backward compatibility)
|
| 331 |
r"buscar\s+juego\s+(.+)",
|
| 332 |
r"busca\s+juego\s+(.+)",
|
| 333 |
r"juego\s+de\s+roblox\s+(.+)",
|
| 334 |
r"buscar\s+(.+)\s+en\s+roblox",
|
|
|
|
| 335 |
r"informaciรณn\s+del\s+juego\s+(.+)",
|
|
|
|
| 336 |
]
|
| 337 |
|
| 338 |
+
def detect_roblox(message):
|
| 339 |
+
text = message.lower().strip()
|
| 340 |
+
for pattern in PLAYER_PATTERNS:
|
| 341 |
+
m = re.search(pattern, text)
|
| 342 |
if m:
|
| 343 |
+
return "player", m.group(1).strip()
|
| 344 |
+
for pattern in GAME_PATTERNS:
|
| 345 |
+
m = re.search(pattern, text)
|
| 346 |
if m:
|
| 347 |
+
return "game", m.group(1).strip()
|
| 348 |
return None, None
|
| 349 |
|
| 350 |
+
# โโ FINAL RESPONSE (non-streaming, used by neo_rest.py) โโโโโโโโโโโโโโโโโโโโโโโ
|
| 351 |
|
| 352 |
+
def final_response(message, history):
|
| 353 |
+
text = message.strip().lower()
|
| 354 |
|
| 355 |
+
if is_calculator_request(message):
|
| 356 |
+
name = extract_username(history)
|
| 357 |
return (
|
| 358 |
+
f"Sure! ๐ {name}, here's our calculator:\n\n"
|
| 359 |
+
"๐งฎ **NEO-1 Virtual Calculator**\n"
|
| 360 |
+
"Type any math operation and I'll solve it instantly.\n\n"
|
| 361 |
+
"**Examples:** `5 + 3`, `12 * 7`, `100 / 4`, `2 ** 8` (power)\n\n"
|
| 362 |
+
"_Type your operation or say 'exit calculator' to go back._"
|
| 363 |
)
|
| 364 |
|
| 365 |
+
if text in ("exit calculator", "close calculator", "quit calculator", "back to chat"):
|
| 366 |
+
return "Alright, back to normal chat. Ask me anything! ๐"
|
| 367 |
|
| 368 |
+
if calculator_mode_active(history) or is_math_operation(message):
|
| 369 |
+
result = solve_operation(message)
|
| 370 |
+
if result is not None:
|
| 371 |
+
return format_result(message, result)
|
| 372 |
|
| 373 |
+
roblox_type, roblox_name = detect_roblox(message)
|
| 374 |
|
| 375 |
+
if roblox_type == "player":
|
| 376 |
+
data = search_player(roblox_name)
|
| 377 |
+
return format_player(data)
|
| 378 |
|
| 379 |
+
if roblox_type == "game":
|
| 380 |
+
data = search_game(roblox_name)
|
| 381 |
+
result = format_game(data)
|
| 382 |
+
if data and "error" not in data:
|
| 383 |
+
explanation = generate_game_explanation(data)
|
| 384 |
+
result = result + "\n\n๐ก **What is this game about?**\n" + explanation
|
| 385 |
+
return result
|
| 386 |
|
| 387 |
+
response = find_custom_response(message)
|
| 388 |
+
if response:
|
| 389 |
+
return response
|
| 390 |
|
| 391 |
+
# โโ NEO-2: web search fallback โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 392 |
+
web_result = web_search(message)
|
| 393 |
+
if web_result.get("found"):
|
| 394 |
+
summary = summarize(message, web_result)
|
| 395 |
+
if summary:
|
| 396 |
+
return summary
|
| 397 |
|
| 398 |
+
return "๐ค I couldn't find information on that topic in my knowledge base or on the web. Try rephrasing your question."
|
chat-app/matematicas.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
| 1 |
import re
|
| 2 |
import math
|
| 3 |
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
r"calculadora virtual",
|
| 6 |
r"calculadora",
|
| 7 |
r"abrir calculadora",
|
|
@@ -11,28 +19,38 @@ PATRONES_CALCULADORA = [
|
|
| 11 |
r"modo calculadora",
|
| 12 |
]
|
| 13 |
|
| 14 |
-
def
|
| 15 |
-
|
| 16 |
-
for
|
| 17 |
-
if re.search(
|
| 18 |
return True
|
| 19 |
return False
|
| 20 |
|
| 21 |
-
def
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
return True
|
| 30 |
-
if re.match(r'^\s*[\d\s\+\-\*\/\.\(\)\%\^]+\s*$',
|
| 31 |
return True
|
| 32 |
return False
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
expr =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
expr = expr.replace("por", "*").replace("entre", "/")
|
| 37 |
expr = expr.replace("mรกs", "+").replace("mas", "+")
|
| 38 |
expr = expr.replace("menos", "-").replace("elevado a", "**")
|
|
@@ -43,58 +61,62 @@ def resolver_operacion(expresion):
|
|
| 43 |
if not expr or not re.search(r'\d', expr):
|
| 44 |
return None
|
| 45 |
try:
|
| 46 |
-
|
| 47 |
"__builtins__": {},
|
| 48 |
"abs": abs, "round": round,
|
| 49 |
"sqrt": math.sqrt, "pow": pow,
|
| 50 |
"pi": math.pi, "e": math.e,
|
| 51 |
}
|
| 52 |
-
|
| 53 |
-
if isinstance(
|
| 54 |
-
return int(
|
| 55 |
-
if isinstance(
|
| 56 |
-
return round(
|
| 57 |
-
return
|
| 58 |
except Exception:
|
| 59 |
return None
|
| 60 |
|
| 61 |
-
def
|
| 62 |
-
if
|
| 63 |
-
return "โ
|
| 64 |
-
|
| 65 |
-
"๐งฎ **
|
| 66 |
"",
|
| 67 |
-
f"๐ฅ **
|
| 68 |
-
f"๐ค **
|
| 69 |
"",
|
| 70 |
-
"
|
| 71 |
]
|
| 72 |
-
return "\n".join(
|
| 73 |
|
| 74 |
-
def
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
r"me llamo\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 77 |
r"mi nombre es\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 78 |
r"soy\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 79 |
r"llรกmame\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 80 |
-
r"llamame\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 81 |
]
|
| 82 |
-
for msg in
|
| 83 |
if isinstance(msg, dict):
|
| 84 |
if msg.get("role") != "user":
|
| 85 |
continue
|
| 86 |
content = msg.get("content") or ""
|
| 87 |
if isinstance(content, list):
|
| 88 |
-
|
| 89 |
-
|
| 90 |
else:
|
| 91 |
-
|
| 92 |
else:
|
| 93 |
-
|
| 94 |
-
for
|
| 95 |
-
m = re.search(
|
| 96 |
if m:
|
| 97 |
-
|
| 98 |
-
if
|
| 99 |
-
return
|
| 100 |
-
return "
|
|
|
|
| 1 |
import re
|
| 2 |
import math
|
| 3 |
|
| 4 |
+
CALCULATOR_PATTERNS = [
|
| 5 |
+
r"virtual calculator",
|
| 6 |
+
r"\bcalculator\b",
|
| 7 |
+
r"open calculator",
|
| 8 |
+
r"use calculator",
|
| 9 |
+
r"i want to calculate",
|
| 10 |
+
r"i need a calculator",
|
| 11 |
+
r"calculator mode",
|
| 12 |
+
# Spanish patterns (kept for backward compatibility)
|
| 13 |
r"calculadora virtual",
|
| 14 |
r"calculadora",
|
| 15 |
r"abrir calculadora",
|
|
|
|
| 19 |
r"modo calculadora",
|
| 20 |
]
|
| 21 |
|
| 22 |
+
def is_calculator_request(message):
|
| 23 |
+
text = message.lower().strip()
|
| 24 |
+
for pattern in CALCULATOR_PATTERNS:
|
| 25 |
+
if re.search(pattern, text):
|
| 26 |
return True
|
| 27 |
return False
|
| 28 |
|
| 29 |
+
def is_math_operation(message):
|
| 30 |
+
text = message.strip()
|
| 31 |
+
# English word replacements
|
| 32 |
+
text = text.replace("times", "*").replace("divided by", "/")
|
| 33 |
+
text = text.replace("plus", "+").replace("minus", "-")
|
| 34 |
+
text = text.replace("to the power of", "**").replace("squared", "**2").replace("cubed", "**3")
|
| 35 |
+
# Spanish word replacements (backward compatibility)
|
| 36 |
+
text = text.replace("por", "*").replace("entre", "/")
|
| 37 |
+
text = text.replace("mรกs", "+").replace("mas", "+")
|
| 38 |
+
text = text.replace("menos", "-").replace("elevado a", "**")
|
| 39 |
+
text = text.replace("al cuadrado", "**2").replace("al cubo", "**3")
|
| 40 |
+
text = re.sub(r'\bx\b', '*', text)
|
| 41 |
+
if re.search(r'\d', text) and re.search(r'[\+\-\*\/\%\^]', text):
|
| 42 |
return True
|
| 43 |
+
if re.match(r'^\s*[\d\s\+\-\*\/\.\(\)\%\^]+\s*$', text) and re.search(r'\d', text):
|
| 44 |
return True
|
| 45 |
return False
|
| 46 |
|
| 47 |
+
def solve_operation(expression):
|
| 48 |
+
expr = expression.strip()
|
| 49 |
+
# English word replacements
|
| 50 |
+
expr = expr.replace("times", "*").replace("divided by", "/")
|
| 51 |
+
expr = expr.replace("plus", "+").replace("minus", "-")
|
| 52 |
+
expr = expr.replace("to the power of", "**").replace("squared", "**2").replace("cubed", "**3")
|
| 53 |
+
# Spanish word replacements (backward compatibility)
|
| 54 |
expr = expr.replace("por", "*").replace("entre", "/")
|
| 55 |
expr = expr.replace("mรกs", "+").replace("mas", "+")
|
| 56 |
expr = expr.replace("menos", "-").replace("elevado a", "**")
|
|
|
|
| 61 |
if not expr or not re.search(r'\d', expr):
|
| 62 |
return None
|
| 63 |
try:
|
| 64 |
+
safe_namespace = {
|
| 65 |
"__builtins__": {},
|
| 66 |
"abs": abs, "round": round,
|
| 67 |
"sqrt": math.sqrt, "pow": pow,
|
| 68 |
"pi": math.pi, "e": math.e,
|
| 69 |
}
|
| 70 |
+
result = eval(expr, safe_namespace)
|
| 71 |
+
if isinstance(result, float) and result == int(result):
|
| 72 |
+
return int(result)
|
| 73 |
+
if isinstance(result, float):
|
| 74 |
+
return round(result, 6)
|
| 75 |
+
return result
|
| 76 |
except Exception:
|
| 77 |
return None
|
| 78 |
|
| 79 |
+
def format_result(original_expression, result):
|
| 80 |
+
if result is None:
|
| 81 |
+
return "โ I couldn't solve that operation. Make sure it's written correctly, e.g.: `5 + 3`, `12 * 4`, `100 / 5`."
|
| 82 |
+
lines = [
|
| 83 |
+
"๐งฎ **NEO-1 Calculator**",
|
| 84 |
"",
|
| 85 |
+
f"๐ฅ **Operation:** `{original_expression.strip()}`",
|
| 86 |
+
f"๐ค **Result:** `{result}`",
|
| 87 |
"",
|
| 88 |
+
"_You can type another operation or say 'exit calculator' to go back to normal chat._"
|
| 89 |
]
|
| 90 |
+
return "\n".join(lines)
|
| 91 |
|
| 92 |
+
def extract_username(history):
|
| 93 |
+
name_patterns = [
|
| 94 |
+
r"my name is\s+([A-Za-z]+)",
|
| 95 |
+
r"i am\s+([A-Za-z]+)",
|
| 96 |
+
r"call me\s+([A-Za-z]+)",
|
| 97 |
+
r"i'm\s+([A-Za-z]+)",
|
| 98 |
+
# Spanish patterns (backward compatibility)
|
| 99 |
r"me llamo\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 100 |
r"mi nombre es\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 101 |
r"soy\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
| 102 |
r"llรกmame\s+([A-Za-zรกรฉรญรณรบรรรรรรฑร]+)",
|
|
|
|
| 103 |
]
|
| 104 |
+
for msg in history:
|
| 105 |
if isinstance(msg, dict):
|
| 106 |
if msg.get("role") != "user":
|
| 107 |
continue
|
| 108 |
content = msg.get("content") or ""
|
| 109 |
if isinstance(content, list):
|
| 110 |
+
parts = [b.get("text", "") if isinstance(b, dict) else str(b) for b in content]
|
| 111 |
+
user_message = " ".join(parts).lower()
|
| 112 |
else:
|
| 113 |
+
user_message = str(content).lower()
|
| 114 |
else:
|
| 115 |
+
user_message = (msg[0] or "").lower()
|
| 116 |
+
for pattern in name_patterns:
|
| 117 |
+
m = re.search(pattern, user_message)
|
| 118 |
if m:
|
| 119 |
+
name = m.group(1).strip().capitalize()
|
| 120 |
+
if name.lower() not in ("a", "an", "the", "un", "una", "el", "la", "de", "que", "y"):
|
| 121 |
+
return name
|
| 122 |
+
return "user"
|
chat-app/neo_rest.py
CHANGED
|
@@ -5,7 +5,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
| 5 |
import sys
|
| 6 |
import os
|
| 7 |
sys.path.insert(0, os.path.dirname(__file__))
|
| 8 |
-
from logica import
|
| 9 |
|
| 10 |
NEO_REST_PORT = 5001
|
| 11 |
|
|
@@ -21,42 +21,42 @@ class NeoAPIHandler(BaseHTTPRequestHandler):
|
|
| 21 |
def do_POST(self):
|
| 22 |
if self.path == "/chat":
|
| 23 |
try:
|
| 24 |
-
length
|
| 25 |
-
raw
|
| 26 |
-
data
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
|
| 31 |
-
if not
|
| 32 |
-
self._send_json(400, {"error": "
|
| 33 |
return
|
| 34 |
|
| 35 |
-
|
| 36 |
self._send_json(200, {
|
| 37 |
-
"response":
|
| 38 |
-
"model":
|
| 39 |
-
"status":
|
| 40 |
})
|
| 41 |
except json.JSONDecodeError:
|
| 42 |
-
self._send_json(400, {"error": "
|
| 43 |
except Exception as e:
|
| 44 |
self._send_json(500, {"error": str(e)})
|
| 45 |
else:
|
| 46 |
-
self._send_json(404, {"error": "
|
| 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": "
|
| 53 |
|
| 54 |
def log_message(self, format, *args):
|
| 55 |
pass
|
| 56 |
|
| 57 |
-
def
|
| 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]
|
| 62 |
return server
|
|
|
|
| 5 |
import sys
|
| 6 |
import os
|
| 7 |
sys.path.insert(0, os.path.dirname(__file__))
|
| 8 |
+
from logica import final_response
|
| 9 |
|
| 10 |
NEO_REST_PORT = 5001
|
| 11 |
|
|
|
|
| 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 |
+
message = str(data.get("message", "")).strip()
|
| 29 |
+
history = data.get("history", [])
|
| 30 |
|
| 31 |
+
if not message:
|
| 32 |
+
self._send_json(400, {"error": "The 'message' field is required."})
|
| 33 |
return
|
| 34 |
|
| 35 |
+
response = final_response(message, history)
|
| 36 |
self._send_json(200, {
|
| 37 |
+
"response": response,
|
| 38 |
+
"model": "mdfjbots-neo-1",
|
| 39 |
+
"status": "ok",
|
| 40 |
})
|
| 41 |
except json.JSONDecodeError:
|
| 42 |
+
self._send_json(400, {"error": "Invalid JSON."})
|
| 43 |
except Exception as e:
|
| 44 |
self._send_json(500, {"error": str(e)})
|
| 45 |
else:
|
| 46 |
+
self._send_json(404, {"error": "Route not found."})
|
| 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": "Route not found."})
|
| 53 |
|
| 54 |
def log_message(self, format, *args):
|
| 55 |
pass
|
| 56 |
|
| 57 |
+
def start_server():
|
| 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] Running on http://0.0.0.0:{NEO_REST_PORT}")
|
| 62 |
return server
|
chat-app/resumidor.py
CHANGED
|
@@ -1,182 +1,171 @@
|
|
| 1 |
"""
|
| 2 |
-
resumidor.py โ NEO-2
|
| 3 |
-
---------------------------------------------------
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
1.
|
| 9 |
-
2.
|
| 10 |
-
3.
|
| 11 |
-
4.
|
| 12 |
"""
|
| 13 |
|
| 14 |
import re
|
| 15 |
import random
|
| 16 |
import unicodedata
|
| 17 |
|
| 18 |
-
# โโ
|
| 19 |
|
| 20 |
-
|
| 21 |
-
"
|
| 22 |
-
"
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
]
|
| 27 |
|
| 28 |
-
|
| 29 |
-
"
|
| 30 |
-
"
|
| 31 |
]
|
| 32 |
|
| 33 |
-
|
| 34 |
"",
|
| 35 |
-
"\n\
|
| 36 |
-
"\n\
|
| 37 |
-
"\n\
|
| 38 |
"",
|
| 39 |
]
|
| 40 |
|
| 41 |
-
# โโ
|
| 42 |
|
| 43 |
-
def
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
return
|
| 47 |
|
| 48 |
-
def
|
| 49 |
-
"""
|
| 50 |
-
|
| 51 |
-
# Quitar frases que suenan a clickbait
|
| 52 |
clickbait = [
|
| 53 |
-
r'
|
| 54 |
-
r'
|
| 55 |
]
|
| 56 |
-
for
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
norm = _normalizar(oracion)
|
| 72 |
score = sum(1.5 for t in query_tokens if t in norm)
|
| 73 |
-
|
| 74 |
-
if 50 <=
|
| 75 |
score += 1.0
|
| 76 |
-
elif
|
| 77 |
score -= 0.5
|
| 78 |
return score
|
| 79 |
|
| 80 |
-
def
|
| 81 |
"""
|
| 82 |
-
|
| 83 |
-
|
| 84 |
"""
|
| 85 |
-
|
| 86 |
for s in snippets:
|
| 87 |
-
|
| 88 |
|
| 89 |
-
if not
|
| 90 |
return []
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
if len(seleccionadas) >= n:
|
| 103 |
break
|
| 104 |
|
| 105 |
-
return
|
| 106 |
|
| 107 |
-
def
|
| 108 |
"""
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
"""
|
| 113 |
-
|
| 114 |
|
| 115 |
-
|
| 116 |
-
lead = _limpiar_oracion(titulo_resultado)
|
| 117 |
if lead:
|
| 118 |
-
|
| 119 |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
partes.append(conector + hecho_limpio)
|
| 128 |
|
| 129 |
-
return " ".join(
|
| 130 |
|
| 131 |
-
# โโ
|
| 132 |
|
| 133 |
-
def
|
| 134 |
"""
|
| 135 |
-
|
| 136 |
-
|
| 137 |
"""
|
| 138 |
-
|
| 139 |
-
abstract =
|
| 140 |
-
snippets =
|
| 141 |
-
url =
|
| 142 |
|
| 143 |
-
if not
|
| 144 |
return None
|
| 145 |
|
| 146 |
-
query_tokens =
|
| 147 |
|
| 148 |
-
|
| 149 |
-
todas_fuentes = []
|
| 150 |
if abstract:
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
hechos = _extraer_hechos(todas_fuentes, query_tokens, n=max_hechos)
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
|
| 159 |
-
if not
|
| 160 |
return None
|
| 161 |
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
cierre = random.choice(_CIERRES)
|
| 167 |
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
origen = resultado.get("origen", "desconocido")
|
| 171 |
|
| 172 |
if url:
|
| 173 |
-
if
|
| 174 |
-
|
| 175 |
-
elif
|
| 176 |
-
|
| 177 |
else:
|
| 178 |
-
|
| 179 |
else:
|
| 180 |
-
|
| 181 |
|
| 182 |
-
return f"{
|
|
|
|
| 1 |
"""
|
| 2 |
+
resumidor.py โ NEO-2 Abstractive summarization engine
|
| 3 |
+
------------------------------------------------------
|
| 4 |
+
Takes raw web search data and builds a prose response
|
| 5 |
+
using its own words โ it does not copy snippets verbatim.
|
| 6 |
+
|
| 7 |
+
Strategy (no LLM required):
|
| 8 |
+
1. The result title is usually the best single-line summary.
|
| 9 |
+
2. Extract key facts from snippets (dates, places, names, roles).
|
| 10 |
+
3. Build new sentences combining those facts with templates.
|
| 11 |
+
4. Present everything as a natural paragraph, not a copied bullet list.
|
| 12 |
"""
|
| 13 |
|
| 14 |
import re
|
| 15 |
import random
|
| 16 |
import unicodedata
|
| 17 |
|
| 18 |
+
# โโ Response headers and closings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 19 |
|
| 20 |
+
_HEADERS = [
|
| 21 |
+
"Here's what I found about **{topic}**:",
|
| 22 |
+
"Here's what I know about **{topic}**:",
|
| 23 |
+
"Let me tell you about **{topic}**:",
|
| 24 |
+
"This is what I found about **{topic}**:",
|
| 25 |
+
"Based on up-to-date information from the web:",
|
| 26 |
]
|
| 27 |
|
| 28 |
+
_CONNECTORS = [
|
| 29 |
+
"Also, ", "It's also known that ", "On the other hand, ",
|
| 30 |
+
"Worth mentioning that ", "Additionally, ", "Notably, ",
|
| 31 |
]
|
| 32 |
|
| 33 |
+
_CLOSINGS = [
|
| 34 |
"",
|
| 35 |
+
"\n\nWould you like me to go deeper on any point? ๐",
|
| 36 |
+
"\n\nNeed more details?",
|
| 37 |
+
"\n\nHope that was helpful! ๐",
|
| 38 |
"",
|
| 39 |
]
|
| 40 |
|
| 41 |
+
# โโ Utilities โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 42 |
|
| 43 |
+
def _normalize(text: str) -> str:
|
| 44 |
+
text = unicodedata.normalize("NFD", text)
|
| 45 |
+
text = "".join(c for c in text if unicodedata.category(c) != "Mn")
|
| 46 |
+
return text.lower()
|
| 47 |
|
| 48 |
+
def _clean_sentence(sentence: str) -> str:
|
| 49 |
+
"""Cleans redundant markers and normalizes whitespace."""
|
| 50 |
+
sentence = re.sub(r'\s+', ' ', sentence).strip()
|
|
|
|
| 51 |
clickbait = [
|
| 52 |
+
r'click here.*', r'read more.*', r'more information.*',
|
| 53 |
+
r'keep reading.*', r'discover more.*', r'find out.*',
|
| 54 |
]
|
| 55 |
+
for pattern in clickbait:
|
| 56 |
+
sentence = re.sub(pattern, '', sentence, flags=re.IGNORECASE)
|
| 57 |
+
if sentence:
|
| 58 |
+
sentence = sentence[0].upper() + sentence[1:]
|
| 59 |
+
if sentence[-1] not in '.!?':
|
| 60 |
+
sentence += '.'
|
| 61 |
+
return sentence.strip()
|
| 62 |
+
|
| 63 |
+
def _split_sentences(text: str) -> list[str]:
|
| 64 |
+
"""Splits text into individual sentences."""
|
| 65 |
+
parts = re.split(r'(?<=[.!?])\s+', text)
|
| 66 |
+
return [p.strip() for p in parts if len(p.strip()) > 30]
|
| 67 |
+
|
| 68 |
+
def _score(sentence: str, query_tokens: list[str]) -> float:
|
| 69 |
+
norm = _normalize(sentence)
|
|
|
|
| 70 |
score = sum(1.5 for t in query_tokens if t in norm)
|
| 71 |
+
length = len(sentence)
|
| 72 |
+
if 50 <= length <= 200:
|
| 73 |
score += 1.0
|
| 74 |
+
elif length < 50:
|
| 75 |
score -= 0.5
|
| 76 |
return score
|
| 77 |
|
| 78 |
+
def _extract_facts(snippets: list[str], query_tokens: list[str], n: int = 3) -> list[str]:
|
| 79 |
"""
|
| 80 |
+
Extracts the most informative sentences from snippets,
|
| 81 |
+
scored by relevance to the query.
|
| 82 |
"""
|
| 83 |
+
all_sentences = []
|
| 84 |
for s in snippets:
|
| 85 |
+
all_sentences.extend(_split_sentences(s))
|
| 86 |
|
| 87 |
+
if not all_sentences:
|
| 88 |
return []
|
| 89 |
|
| 90 |
+
scored = sorted(all_sentences, key=lambda s: -_score(s, query_tokens))
|
| 91 |
+
|
| 92 |
+
selected = []
|
| 93 |
+
seen = set()
|
| 94 |
+
for sentence in scored:
|
| 95 |
+
key = _normalize(sentence)[:50]
|
| 96 |
+
if key not in seen:
|
| 97 |
+
seen.add(key)
|
| 98 |
+
selected.append(sentence)
|
| 99 |
+
if len(selected) >= n:
|
|
|
|
| 100 |
break
|
| 101 |
|
| 102 |
+
return selected
|
| 103 |
|
| 104 |
+
def _build_paragraph(result_title: str, facts: list[str]) -> str:
|
| 105 |
"""
|
| 106 |
+
Builds a prose paragraph from the title and extracted facts.
|
| 107 |
+
The title acts as the lead sentence.
|
| 108 |
+
Facts are integrated with natural connectors.
|
| 109 |
"""
|
| 110 |
+
parts = []
|
| 111 |
|
| 112 |
+
lead = _clean_sentence(result_title)
|
|
|
|
| 113 |
if lead:
|
| 114 |
+
parts.append(lead)
|
| 115 |
|
| 116 |
+
used_connectors = random.sample(_CONNECTORS, min(len(facts), len(_CONNECTORS)))
|
| 117 |
+
for i, fact in enumerate(facts):
|
| 118 |
+
clean_fact = _clean_sentence(fact)
|
| 119 |
+
if not clean_fact or clean_fact.lower()[:40] in lead.lower():
|
| 120 |
+
continue
|
| 121 |
+
connector = used_connectors[i] if i < len(used_connectors) else ""
|
| 122 |
+
parts.append(connector + clean_fact)
|
|
|
|
| 123 |
|
| 124 |
+
return " ".join(parts)
|
| 125 |
|
| 126 |
+
# โโ Main function โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 127 |
|
| 128 |
+
def summarize(query: str, result: dict, max_facts: int = 3) -> str | None:
|
| 129 |
"""
|
| 130 |
+
Generates an original response from web search results.
|
| 131 |
+
Does not copy snippets โ builds new prose from key facts.
|
| 132 |
"""
|
| 133 |
+
title = result.get("title", "").strip()
|
| 134 |
+
abstract = result.get("abstract", "").strip()
|
| 135 |
+
snippets = result.get("snippets", [])
|
| 136 |
+
url = result.get("url", "").strip()
|
| 137 |
|
| 138 |
+
if not title and not abstract and not snippets:
|
| 139 |
return None
|
| 140 |
|
| 141 |
+
query_tokens = _normalize(query).split()
|
| 142 |
|
| 143 |
+
all_sources = []
|
|
|
|
| 144 |
if abstract:
|
| 145 |
+
all_sources.append(abstract)
|
| 146 |
+
all_sources.extend(snippets)
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
facts = _extract_facts(all_sources, query_tokens, n=max_facts)
|
| 149 |
+
paragraph = _build_paragraph(title, facts)
|
| 150 |
|
| 151 |
+
if not paragraph.strip():
|
| 152 |
return None
|
| 153 |
|
| 154 |
+
topic = title.split(":")[0].split("โ")[0].strip() if title else query
|
| 155 |
+
header = random.choice(_HEADERS).format(topic=topic)
|
| 156 |
+
closing = random.choice(_CLOSINGS)
|
|
|
|
|
|
|
| 157 |
|
| 158 |
+
license_ = result.get("license", "")
|
| 159 |
+
origin = result.get("origin", "unknown")
|
|
|
|
| 160 |
|
| 161 |
if url:
|
| 162 |
+
if license_ and license_ != "Unknown":
|
| 163 |
+
footer = f"\n\n๐ Source: {url}\n๐ License: {license_}"
|
| 164 |
+
elif origin == "unknown":
|
| 165 |
+
footer = f"\n\n๐ Source: {url}\nโ ๏ธ _Unknown license โ content used for reference only._"
|
| 166 |
else:
|
| 167 |
+
footer = f"\n\n๐ Source: {url}"
|
| 168 |
else:
|
| 169 |
+
footer = ""
|
| 170 |
|
| 171 |
+
return f"{header}\n\n{paragraph}{footer}{closing}".strip()
|
chat-app/roblox_api.py
CHANGED
|
@@ -5,175 +5,191 @@ HEADERS = {
|
|
| 5 |
"Accept": "application/json",
|
| 6 |
}
|
| 7 |
|
| 8 |
-
def
|
| 9 |
try:
|
| 10 |
-
url
|
| 11 |
-
resp = requests.post(
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
return None
|
| 16 |
-
|
| 17 |
-
user_id =
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
return {
|
| 22 |
-
"id":
|
| 23 |
-
"
|
| 24 |
-
"
|
| 25 |
-
"
|
| 26 |
-
"
|
| 27 |
-
"is_banned":
|
| 28 |
-
"avatar":
|
| 29 |
-
"
|
| 30 |
-
"
|
| 31 |
-
"
|
| 32 |
}
|
| 33 |
except Exception as e:
|
| 34 |
return {"error": str(e)}
|
| 35 |
|
| 36 |
-
def
|
| 37 |
try:
|
| 38 |
-
url
|
| 39 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 40 |
return resp.json()
|
| 41 |
except Exception:
|
| 42 |
return {}
|
| 43 |
|
| 44 |
-
def
|
| 45 |
try:
|
| 46 |
-
url
|
|
|
|
|
|
|
|
|
|
| 47 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 48 |
-
|
| 49 |
-
return
|
| 50 |
except Exception:
|
| 51 |
return None
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
|
| 55 |
try:
|
| 56 |
-
r = requests.get(
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
except Exception:
|
| 59 |
-
|
| 60 |
try:
|
| 61 |
-
r = requests.get(
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
except Exception:
|
| 64 |
-
|
| 65 |
try:
|
| 66 |
-
r = requests.get(
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
| 68 |
except Exception:
|
| 69 |
-
|
| 70 |
-
return
|
| 71 |
|
| 72 |
-
def
|
| 73 |
try:
|
| 74 |
-
url
|
| 75 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
if not
|
| 79 |
return None
|
| 80 |
-
universe_id =
|
| 81 |
if not universe_id:
|
| 82 |
return None
|
| 83 |
-
|
| 84 |
-
return detalle
|
| 85 |
except Exception as e:
|
| 86 |
return {"error": str(e)}
|
| 87 |
|
| 88 |
-
def
|
| 89 |
try:
|
| 90 |
-
url
|
| 91 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if not
|
| 95 |
return None
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
if
|
| 99 |
-
|
| 100 |
return {
|
| 101 |
-
"
|
| 102 |
-
"
|
| 103 |
-
"
|
| 104 |
-
"
|
| 105 |
-
"
|
| 106 |
-
"
|
| 107 |
-
"
|
| 108 |
-
"id":
|
| 109 |
}
|
| 110 |
except Exception as e:
|
| 111 |
return {"error": str(e)}
|
| 112 |
|
| 113 |
-
def
|
| 114 |
-
if not
|
| 115 |
-
return "
|
| 116 |
-
if "error" in
|
| 117 |
-
return f"
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
if
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
"
|
| 130 |
"",
|
| 131 |
-
f"๐ค **
|
| 132 |
-
f"โ๏ธ **
|
| 133 |
-
f"๐ **ID:** {
|
| 134 |
-
f"๐ **
|
| 135 |
-
f"๐
**
|
| 136 |
-
f"๐ซ **
|
| 137 |
"",
|
| 138 |
-
"๐ **
|
| 139 |
-
f"๐ซ **
|
| 140 |
-
f"๐ฅ **
|
| 141 |
-
f"โก๏ธ **
|
| 142 |
]
|
| 143 |
|
| 144 |
-
if
|
| 145 |
-
|
| 146 |
|
| 147 |
-
return "\n".join(
|
| 148 |
|
| 149 |
-
def
|
| 150 |
-
if not
|
| 151 |
-
return "
|
| 152 |
-
if "error" in
|
| 153 |
-
return f"
|
| 154 |
|
| 155 |
-
|
| 156 |
try:
|
| 157 |
-
|
| 158 |
except Exception:
|
| 159 |
-
|
| 160 |
|
| 161 |
-
|
| 162 |
try:
|
| 163 |
-
|
| 164 |
except Exception:
|
| 165 |
-
|
| 166 |
|
| 167 |
-
|
| 168 |
-
"
|
| 169 |
"",
|
| 170 |
-
f"๐ฎ **
|
| 171 |
-
f"๐ญ **
|
| 172 |
-
f"๐๏ธ **
|
| 173 |
-
f"๐ **
|
| 174 |
-
f"๐ **
|
| 175 |
"",
|
| 176 |
-
f"๐ค **
|
| 177 |
-
f"๐ฅ **
|
| 178 |
]
|
| 179 |
-
return "\n".join(
|
|
|
|
| 5 |
"Accept": "application/json",
|
| 6 |
}
|
| 7 |
|
| 8 |
+
def search_player(name):
|
| 9 |
try:
|
| 10 |
+
url = "https://users.roblox.com/v1/usernames/users"
|
| 11 |
+
resp = requests.post(
|
| 12 |
+
url,
|
| 13 |
+
json={"usernames": [name], "excludeBannedUsers": False},
|
| 14 |
+
headers=HEADERS,
|
| 15 |
+
timeout=8,
|
| 16 |
+
)
|
| 17 |
+
data = resp.json()
|
| 18 |
+
users = data.get("data", [])
|
| 19 |
+
if not users:
|
| 20 |
return None
|
| 21 |
+
user = users[0]
|
| 22 |
+
user_id = user["id"]
|
| 23 |
+
profile = get_profile(user_id)
|
| 24 |
+
avatar = get_avatar(user_id)
|
| 25 |
+
stats = get_stats(user_id)
|
| 26 |
return {
|
| 27 |
+
"id": user_id,
|
| 28 |
+
"name": user.get("name"),
|
| 29 |
+
"display_name": user.get("displayName"),
|
| 30 |
+
"description": profile.get("description", ""),
|
| 31 |
+
"created": profile.get("created", "Unknown"),
|
| 32 |
+
"is_banned": profile.get("isBanned", False),
|
| 33 |
+
"avatar": avatar,
|
| 34 |
+
"friends": stats.get("friends", "N/A"),
|
| 35 |
+
"followers": stats.get("followers", "N/A"),
|
| 36 |
+
"following": stats.get("following", "N/A"),
|
| 37 |
}
|
| 38 |
except Exception as e:
|
| 39 |
return {"error": str(e)}
|
| 40 |
|
| 41 |
+
def get_profile(user_id):
|
| 42 |
try:
|
| 43 |
+
url = f"https://users.roblox.com/v1/users/{user_id}"
|
| 44 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 45 |
return resp.json()
|
| 46 |
except Exception:
|
| 47 |
return {}
|
| 48 |
|
| 49 |
+
def get_avatar(user_id):
|
| 50 |
try:
|
| 51 |
+
url = (
|
| 52 |
+
f"https://thumbnails.roblox.com/v1/users/avatar-headshot"
|
| 53 |
+
f"?userIds={user_id}&size=420x420&format=Png&isCircular=false"
|
| 54 |
+
)
|
| 55 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 56 |
+
data = resp.json()
|
| 57 |
+
return data.get("data", [{}])[0].get("imageUrl", None)
|
| 58 |
except Exception:
|
| 59 |
return None
|
| 60 |
|
| 61 |
+
def get_stats(user_id):
|
| 62 |
+
result = {}
|
| 63 |
try:
|
| 64 |
+
r = requests.get(
|
| 65 |
+
f"https://friends.roblox.com/v1/users/{user_id}/friends/count",
|
| 66 |
+
headers=HEADERS, timeout=8,
|
| 67 |
+
)
|
| 68 |
+
result["friends"] = r.json().get("count", "N/A")
|
| 69 |
except Exception:
|
| 70 |
+
result["friends"] = "N/A"
|
| 71 |
try:
|
| 72 |
+
r = requests.get(
|
| 73 |
+
f"https://friends.roblox.com/v1/users/{user_id}/followers/count",
|
| 74 |
+
headers=HEADERS, timeout=8,
|
| 75 |
+
)
|
| 76 |
+
result["followers"] = r.json().get("count", "N/A")
|
| 77 |
except Exception:
|
| 78 |
+
result["followers"] = "N/A"
|
| 79 |
try:
|
| 80 |
+
r = requests.get(
|
| 81 |
+
f"https://friends.roblox.com/v1/users/{user_id}/followings/count",
|
| 82 |
+
headers=HEADERS, timeout=8,
|
| 83 |
+
)
|
| 84 |
+
result["following"] = r.json().get("count", "N/A")
|
| 85 |
except Exception:
|
| 86 |
+
result["following"] = "N/A"
|
| 87 |
+
return result
|
| 88 |
|
| 89 |
+
def search_game(name):
|
| 90 |
try:
|
| 91 |
+
url = f"https://games.roblox.com/v1/games/list?keyword={name}&maxRows=5&startRows=0"
|
| 92 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 93 |
+
data = resp.json()
|
| 94 |
+
games = data.get("games", [])
|
| 95 |
+
if not games:
|
| 96 |
return None
|
| 97 |
+
universe_id = games[0].get("universeId")
|
| 98 |
if not universe_id:
|
| 99 |
return None
|
| 100 |
+
return get_game_details(universe_id)
|
|
|
|
| 101 |
except Exception as e:
|
| 102 |
return {"error": str(e)}
|
| 103 |
|
| 104 |
+
def get_game_details(universe_id):
|
| 105 |
try:
|
| 106 |
+
url = f"https://games.roblox.com/v1/games?universeIds={universe_id}"
|
| 107 |
resp = requests.get(url, headers=HEADERS, timeout=8)
|
| 108 |
+
data = resp.json()
|
| 109 |
+
games = data.get("data", [])
|
| 110 |
+
if not games:
|
| 111 |
return None
|
| 112 |
+
g = games[0]
|
| 113 |
+
last_update = g.get("updated", "Unknown")
|
| 114 |
+
if last_update and last_update != "Unknown":
|
| 115 |
+
last_update = last_update[:10]
|
| 116 |
return {
|
| 117 |
+
"name": g.get("name"),
|
| 118 |
+
"genre": g.get("genre", "Uncategorized"),
|
| 119 |
+
"visits": g.get("visits", 0),
|
| 120 |
+
"last_update": last_update,
|
| 121 |
+
"description": g.get("description") or "No description.",
|
| 122 |
+
"creator": g.get("creator", {}).get("name", "Unknown"),
|
| 123 |
+
"playing": g.get("playing", 0),
|
| 124 |
+
"id": universe_id,
|
| 125 |
}
|
| 126 |
except Exception as e:
|
| 127 |
return {"error": str(e)}
|
| 128 |
|
| 129 |
+
def format_player(data):
|
| 130 |
+
if not data:
|
| 131 |
+
return "I couldn't find that player on Roblox. Please check that the username is correct."
|
| 132 |
+
if "error" in data:
|
| 133 |
+
return f"An error occurred while searching for the player: {data['error']}"
|
| 134 |
+
|
| 135 |
+
description = data.get("description") or "No description."
|
| 136 |
+
created = data.get("created", "Unknown")
|
| 137 |
+
if created != "Unknown":
|
| 138 |
+
created = created[:10]
|
| 139 |
+
banned = "Yes โ ๏ธ" if data.get("is_banned") else "No โ
"
|
| 140 |
+
friends = data.get("friends", "N/A")
|
| 141 |
+
followers = data.get("followers", "N/A")
|
| 142 |
+
following = data.get("following", "N/A")
|
| 143 |
+
|
| 144 |
+
lines = [
|
| 145 |
+
"Here's the public data for this Roblox player ๐",
|
| 146 |
"",
|
| 147 |
+
f"๐ค **Username:** {data['name']}",
|
| 148 |
+
f"โ๏ธ **Display name:** {data['display_name']}",
|
| 149 |
+
f"๐ **ID:** {data['id']}",
|
| 150 |
+
f"๐ **Description:** {description}",
|
| 151 |
+
f"๐
**Account created:** {created}",
|
| 152 |
+
f"๐ซ **Banned?:** {banned}",
|
| 153 |
"",
|
| 154 |
+
"๐ **Stats:**",
|
| 155 |
+
f"๐ซ **Friends:** {friends}",
|
| 156 |
+
f"๐ฅ **Followers:** {followers}",
|
| 157 |
+
f"โก๏ธ **Following:** {following}",
|
| 158 |
]
|
| 159 |
|
| 160 |
+
if data.get("avatar"):
|
| 161 |
+
lines.append(f"\n๐ผ๏ฟฝ๏ฟฝ **Avatar:** {data['avatar']}")
|
| 162 |
|
| 163 |
+
return "\n".join(lines)
|
| 164 |
|
| 165 |
+
def format_game(data):
|
| 166 |
+
if not data:
|
| 167 |
+
return "I couldn't find that game on Roblox. Try a different name."
|
| 168 |
+
if "error" in data:
|
| 169 |
+
return f"An error occurred while searching for the game: {data['error']}"
|
| 170 |
|
| 171 |
+
visits = data.get("visits", 0)
|
| 172 |
try:
|
| 173 |
+
visits = f"{int(visits):,}"
|
| 174 |
except Exception:
|
| 175 |
+
visits = str(visits)
|
| 176 |
|
| 177 |
+
playing = data.get("playing", 0)
|
| 178 |
try:
|
| 179 |
+
playing = f"{int(playing):,}"
|
| 180 |
except Exception:
|
| 181 |
+
playing = str(playing)
|
| 182 |
|
| 183 |
+
lines = [
|
| 184 |
+
"Roblox game data ๐",
|
| 185 |
"",
|
| 186 |
+
f"๐ฎ **Name:** {data.get('name', 'Unknown')}",
|
| 187 |
+
f"๐ญ **Genre:** {data.get('genre', 'Uncategorized')}",
|
| 188 |
+
f"๐๏ธ **Visits:** {visits}",
|
| 189 |
+
f"๐ **Last update:** {data.get('last_update', 'Unknown')}",
|
| 190 |
+
f"๐ **Description:** {data.get('description', 'No description.')}",
|
| 191 |
"",
|
| 192 |
+
f"๐ค **Creator:** {data.get('creator', 'Unknown')}",
|
| 193 |
+
f"๐ฅ **Currently playing:** {playing}",
|
| 194 |
]
|
| 195 |
+
return "\n".join(lines)
|