add healthcheck to remaining apps

This commit is contained in:
2025-11-25 07:05:14 +00:00
parent eccd53673c
commit 6d9686e373
87 changed files with 850 additions and 632 deletions

View File

@@ -20,13 +20,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
bursatil: ${{ steps.filter.outputs.bursatil }} bursatil: ${{ steps.filter.outputs.bursatil }}
ChatEgresos: ${{ steps.filter.outputs.ChatEgresos }} egresos: ${{ steps.filter.outputs.egresos }}
inversionistas: ${{ steps.filter.outputs.inversionistas }} inversionistas: ${{ steps.filter.outputs.inversionistas }}
normativa: ${{ steps.filter.outputs.normativa }} normativa: ${{ steps.filter.outputs.normativa }}
ocp: ${{ steps.filter.outputs.ocp }} ocp: ${{ steps.filter.outputs.ocp }}
pyme: ${{ steps.filter.outputs.pyme }} pyme: ${{ steps.filter.outputs.pyme }}
riesgos: ${{ steps.filter.outputs.riesgos }} riesgos: ${{ steps.filter.outputs.riesgos }}
Test: ${{ steps.filter.outputs.Test }}
voz-del-cliente: ${{ steps.filter.outputs.voz-del-cliente }} voz-del-cliente: ${{ steps.filter.outputs.voz-del-cliente }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -39,8 +38,8 @@ jobs:
filters: | filters: |
bursatil: bursatil:
- 'apps/bursatil/**' - 'apps/bursatil/**'
ChatEgresos: egresos:
- 'apps/ChatEgresos/**' - 'apps/egresos/**'
inversionistas: inversionistas:
- 'apps/inversionistas/**' - 'apps/inversionistas/**'
normativa: normativa:
@@ -51,8 +50,6 @@ jobs:
- 'apps/pyme/**' - 'apps/pyme/**'
riesgos: riesgos:
- 'apps/riesgos/**' - 'apps/riesgos/**'
Test:
- 'apps/Test/**'
voz-del-cliente: voz-del-cliente:
- 'apps/voz-del-cliente/**' - 'apps/voz-del-cliente/**'
@@ -80,9 +77,9 @@ jobs:
build-args: | build-args: |
PACKAGE=bursatil PACKAGE=bursatil
build-ChatEgresos: build-egresos:
needs: detect-changes needs: detect-changes
if: needs.detect-changes.outputs.ChatEgresos == 'true' if: needs.detect-changes.outputs.egresos == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -99,10 +96,10 @@ jobs:
file: .containers/python/Dockerfile file: .containers/python/Dockerfile
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-ChatEgresos:latest ${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-egresos:latest
${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-ChatEgresos:${{ github.sha }} ${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-egresos:${{ github.sha }}
build-args: | build-args: |
PACKAGE=ChatEgresos PACKAGE=egresos
build-inversionistas: build-inversionistas:
needs: detect-changes needs: detect-changes
@@ -224,30 +221,6 @@ jobs:
build-args: | build-args: |
PACKAGE=riesgos PACKAGE=riesgos
build-Test:
needs: detect-changes
if: needs.detect-changes.outputs.Test == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: .containers/python/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-Test:latest
${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}-Test:${{ github.sha }}
build-args: |
PACKAGE=Test
build-voz-del-cliente: build-voz-del-cliente:
needs: detect-changes needs: detect-changes
if: needs.detect-changes.outputs.voz-del-cliente == 'true' if: needs.detect-changes.outputs.voz-del-cliente == 'true'

View File

@@ -1,18 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

View File

@@ -1,65 +0,0 @@
import { Chat, ChatSidebar } from "@banorte/chat-ui";
import { messageStore } from "./store/messageStore";
import { conversationStore } from "./store/conversationStore";
import { httpRequest } from "./utils/request";
// Assets
import banorteLogo from "./assets/banortelogo.png";
import sidebarMaya from "./assets/sidebar_maya_contigo.png";
import brujulaElipse from "./assets/brujula_elipse.png";
import sendIcon from "./assets/chat_maya_boton_enviar.png";
import userAvatar from "./assets/chat_maya_default_avatar.png";
import botAvatar from "./assets/brujula.png";
function App() {
const { messages, pushMessage } = messageStore();
const {
conversationId,
setConversationId,
setAssistantName,
receivingMsg,
setReceivingMsg
} = conversationStore();
const handleStartConversation = async (user: string, assistant: string): Promise<string> => {
const response = await httpRequest("POST", "/v1/conversation", { user, assistant });
console.log("Conversation id:", response.conversation_id);
return response.conversation_id;
};
const handleFeedback = async (key: string, rating: string): Promise<void> => {
await httpRequest("POST", "/v1/feedback", { key, rating });
};
const assistant = "Maya" + "Test";
return (
<div className="w-screen flex flex-col h-screen min-h-screen scrollbar-none">
<div className="w-full flex">
<ChatSidebar
assistant={assistant}
logoSrc={banorteLogo}
sidebarImageSrc={sidebarMaya}
assistantAvatarSrc={brujulaElipse}
/>
<Chat
assistant={assistant}
messages={messages}
pushMessage={pushMessage}
conversationId={conversationId}
setConversationId={setConversationId}
setAssistantName={setAssistantName}
receivingMsg={receivingMsg}
setReceivingMsg={setReceivingMsg}
onStartConversation={handleStartConversation}
sendIcon={sendIcon}
userAvatar={userAvatar}
botAvatar={botAvatar}
onFeedback={handleFeedback}
/>
</div>
</div>
);
}
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,16 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.markdown a {
color: #0000FF;
text-decoration: underline;
}
.markdown a:hover {
color: #FF0000;
}
.markdown a:visited {
color: #800080;
}

View File

@@ -1,5 +0,0 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -1,19 +0,0 @@
import { create } from "zustand";
interface conversationState {
assistantName: string;
conversationId: string;
receivingMsg: boolean;
setConversationId: (newId: string) => void;
setAssistantName: (newName: string) => void;
setReceivingMsg: (newState: boolean) => void;
}
export const conversationStore = create<conversationState>()((set) => ({
assistantName: "",
conversationId: "",
receivingMsg: false,
setConversationId: (newId) => set({ conversationId: newId }),
setAssistantName: (newName) => set({ assistantName: newName }),
setReceivingMsg: (newState) => set({ receivingMsg: newState }),
}));

View File

@@ -1,14 +0,0 @@
import { create } from "zustand";
interface messageState {
messages: Array<{ user: boolean; content: string }>;
pushMessage: (newMessage: { user: boolean; content: string }) => void;
resetConversation: () => void;
}
export const messageStore = create<messageState>()((set) => ({
messages: [],
pushMessage: (newMessage) =>
set((state) => ({ messages: [...state.messages, newMessage] })),
resetConversation: () => set(() => ({ messages: [] })),
}));

View File

@@ -1,16 +0,0 @@
export async function httpRequest(
method: string,
endpoint: string,
body: object | null,
) {
const url = "/api" + endpoint;
const data = {
method: method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
credentials: "include" as RequestCredentials,
};
return await fetch(url, data).then((response) => response.json());
}

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/gui/main.tsx"></script>
</body>
</html>

View File

@@ -1,40 +0,0 @@
{
"name": "Test",
"private": true,
"version": "0.0.7",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@banorte/chat-ui": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-spring": "^9.7.4",
"rehype-raw": "^7.0.0",
"sse.js": "^2.5.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@iconify-icon/react": "^2.1.0",
"@types/react": "^18.2.67",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"daisyui": "^4.7.3",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.3",
"vite": "^5.2.3"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,18 +0,0 @@
[project]
name = "Test"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12, <4"
dependencies = [
"aiohttp>=3.11.16",
"fastapi>=0.115.6",
"hvac>=2.3.0",
"langchain-azure-ai[opentelemetry]>=0.1.4",
"mongo-memory",
"pydantic-settings>=2.8.1",
"uvicorn>=0.34.0",
]
[tool.uv.sources]
mongo-memory = { workspace = true }

View File

@@ -1,27 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./gui/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
backgroundImage: {
"navigation-pattern": "url('./assets/navigation.webp')",
},
},
},
plugins: [
require("daisyui"),
require("tailwind-scrollbar"),
require("@banorte/chat-ui/tailwind")
],
daisyui: {
themes: [
{
light: {
...require("daisyui/src/theming/themes")["light"],
primary: "red",
secondary: "teal",
},
},
],
},
};

View File

@@ -1,25 +0,0 @@
{
"compilerOptions": {
"target": "ES2023",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable", "ES2021.String"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["gui"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,17 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
host: "0.0.0.0",
port: 3000,
proxy: {
"/api": {
target: "http://localhost:8000",
},
},
allowedHosts: true,
},
});

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-bursatil
namespace: apps
labels:
app: mayacontigo-bursatil
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-bursatil
template:
metadata:
labels:
app: mayacontigo-bursatil
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-bursatil
image: gitea.ia-innovacion.work/innovacion/mayacontigo-bursatil:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-bursatil-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-bursatil-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-bursatil.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-bursatil-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-bursatil-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-bursatil-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-bursatil
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-bursatil-service
labels:
app: mayacontigo-bursatil
spec:
selector:
app: mayacontigo-bursatil
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-egresos
namespace: apps
labels:
app: mayacontigo-egresos
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-egresos
template:
metadata:
labels:
app: mayacontigo-egresos
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-egresos
image: gitea.ia-innovacion.work/innovacion/mayacontigo-egresos:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-egresos-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-egresos-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-egresos.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-egresos-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-egresos-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-egresos-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-egresos
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-egresos-service
labels:
app: mayacontigo-egresos
spec:
selector:
app: mayacontigo-egresos
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -1,11 +1,11 @@
import uuid
import time import time
import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from langfuse import Langfuse from langfuse import Langfuse
from pydantic import BaseModel
from api import services from api import services
from api.agent import Agent from api.agent import Agent
@@ -14,10 +14,11 @@ from api.config import config
# Configurar Langfuse # Configurar Langfuse
langfuse = Langfuse( langfuse = Langfuse(
public_key="pk-lf-49cb04b3-0c7d-475b-8105-ad8b8749ecdd", public_key="pk-lf-49cb04b3-0c7d-475b-8105-ad8b8749ecdd",
secret_key="sk-lf-e02fa322-c709-4d80-bef2-9cb279846a0c", secret_key="sk-lf-e02fa322-c709-4d80-bef2-9cb279846a0c",
host="https://ailogger.azurewebsites.net" host="https://ailogger.azurewebsites.net",
) )
@asynccontextmanager @asynccontextmanager
async def lifespan(_: FastAPI): async def lifespan(_: FastAPI):
await config.init_mongo_db() await config.init_mongo_db()
@@ -39,6 +40,7 @@ class Message(BaseModel):
conversation_id: uuid.UUID conversation_id: uuid.UUID
prompt: str prompt: str
@app.post("/api/v1/message") @app.post("/api/v1/message")
async def send(message: Message): async def send(message: Message):
# Crear trace principal # Crear trace principal
@@ -47,66 +49,68 @@ async def send(message: Message):
session_id=str(message.conversation_id), session_id=str(message.conversation_id),
input={ input={
"prompt": message.prompt, "prompt": message.prompt,
"conversation_id": str(message.conversation_id) "conversation_id": str(message.conversation_id),
} },
) )
def b64_sse(func): def b64_sse(func):
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
response_parts = [] response_parts = []
start_time = time.time() start_time = time.time()
async for chunk in func(*args, **kwargs): async for chunk in func(*args, **kwargs):
if chunk.type == "text" and chunk.content: if chunk.type == "text" and chunk.content:
response_parts.append(str(chunk.content)) response_parts.append(str(chunk.content))
content = chunk.model_dump_json() content = chunk.model_dump_json()
data = f"data: {content}\n\n" data = f"data: {content}\n\n"
yield data yield data
end_time = time.time() end_time = time.time()
latency_ms = round((end_time - start_time) * 1000) latency_ms = round((end_time - start_time) * 1000)
full_response = "".join(response_parts) full_response = "".join(response_parts)
input_tokens = len(message.prompt.split()) * 1.3
input_tokens = len(message.prompt.split()) * 1.3
output_tokens = len(full_response.split()) * 1.3 output_tokens = len(full_response.split()) * 1.3
total_tokens = int(input_tokens + output_tokens) total_tokens = int(input_tokens + output_tokens)
cost_per_1k_input = 0.03
cost_per_1k_input = 0.03 cost_per_1k_output = 0.06
cost_per_1k_output = 0.06 total_cost = (input_tokens / 1000 * cost_per_1k_input) + (
total_cost = (input_tokens/1000 * cost_per_1k_input) + (output_tokens/1000 * cost_per_1k_output) output_tokens / 1000 * cost_per_1k_output
)
trace.update( trace.update(
output={"response": full_response}, output={"response": full_response},
usage={ usage={
"input": int(input_tokens), "input": int(input_tokens),
"output": int(output_tokens), "output": int(output_tokens),
"total": total_tokens, "total": total_tokens,
"unit": "TOKENS" "unit": "TOKENS",
} },
) )
langfuse.score( langfuse.score(
trace_id=trace.id, trace_id=trace.id,
name="latency", name="latency",
value=latency_ms, value=latency_ms,
comment=f"Response time: {latency_ms}ms" comment=f"Response time: {latency_ms}ms",
) )
langfuse.score( langfuse.score(
trace_id=trace.id, trace_id=trace.id,
name="cost", name="cost",
value=round(total_cost, 4), value=round(total_cost, 4),
comment=f"Estimated cost: ${round(total_cost, 4)}" comment=f"Estimated cost: ${round(total_cost, 4)}",
) )
return wrapper return wrapper
sse_stream = b64_sse(services.stream) sse_stream = b64_sse(services.stream)
generator = sse_stream(agent, message.prompt, message.conversation_id) generator = sse_stream(agent, message.prompt, message.conversation_id)
return StreamingResponse(generator, media_type="text/event-stream") return StreamingResponse(generator, media_type="text/event-stream")
@app.get("/")
async def health():
return {"status": "ok"}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -1,5 +1,5 @@
[project] [project]
name = "ChatEgresos" name = "egresos"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-normativa
namespace: apps
labels:
app: mayacontigo-normativa
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-normativa
template:
metadata:
labels:
app: mayacontigo-normativa
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-normativa
image: gitea.ia-innovacion.work/innovacion/mayacontigo-normativa:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-normativa-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-normativa-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-normativa.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-normativa-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-normativa-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-normativa-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-normativa
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-normativa-service
labels:
app: mayacontigo-normativa
spec:
selector:
app: mayacontigo-normativa
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -1,15 +1,14 @@
import uuid
import os import os
import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv # ← Agregar este import
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from pydantic import BaseModel
from langfuse import Langfuse from langfuse import Langfuse
from pydantic import BaseModel
from dotenv import load_dotenv # ← Agregar este import
from api import services from api import services
from api.agent import Agent from api.agent import Agent
@@ -21,8 +20,8 @@ load_dotenv()
# Configurar Langfuse desde variables de entorno # Configurar Langfuse desde variables de entorno
langfuse = Langfuse( langfuse = Langfuse(
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"), public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
secret_key=os.getenv("LANGFUSE_SECRET_KEY"), secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
host=os.getenv("LANGFUSE_HOST") host=os.getenv("LANGFUSE_HOST"),
) )
@@ -34,12 +33,10 @@ PDF_PUBLIC_URLS = {
"Disposiciones de carácter general aplicables a las sociedades controladoras de grupos financieros y subcontroladoras que regulan las materias que corresponden de manera conjunta a las Comisio.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20sociedades%20controladoras%20de%20grupos%20financieros%20y%20subcontroladoras%20que%20regulan%20las%20materias%20que%20corresponden%20de%20manera%20conjunta%20a%20las%20Comisiones%20Nacionales%20Supervisoras.pdf", "Disposiciones de carácter general aplicables a las sociedades controladoras de grupos financieros y subcontroladoras que regulan las materias que corresponden de manera conjunta a las Comisio.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20sociedades%20controladoras%20de%20grupos%20financieros%20y%20subcontroladoras%20que%20regulan%20las%20materias%20que%20corresponden%20de%20manera%20conjunta%20a%20las%20Comisiones%20Nacionales%20Supervisoras.pdf",
"Disposiciones de carácter general aplicables a los fondos de inversión y a las personas que les prestan servicios.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20los%20fondos%20de%20inversi%C3%B3n%20y%20a%20las%20personas%20que%20les%20prestan%20servicios.pdf", "Disposiciones de carácter general aplicables a los fondos de inversión y a las personas que les prestan servicios.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20los%20fondos%20de%20inversi%C3%B3n%20y%20a%20las%20personas%20que%20les%20prestan%20servicios.pdf",
"Ley para la Transparencia y Ordenamiento de los Servicios Financieros.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20para%20la%20Transparencia%20y%20Ordenamiento%20de%20los%20Servicios%20Financieros.pdf", "Ley para la Transparencia y Ordenamiento de los Servicios Financieros.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20para%20la%20Transparencia%20y%20Ordenamiento%20de%20los%20Servicios%20Financieros.pdf",
# Circulares CNBV adicionales # Circulares CNBV adicionales
"circular_servicios_de_inversion.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20entidades%20financieras%20y%20dem%C3%A1s%20personas%20que%20proporcionen%20servicios%20de.pdf", "circular_servicios_de_inversion.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20aplicables%20a%20las%20entidades%20financieras%20y%20dem%C3%A1s%20personas%20que%20proporcionen%20servicios%20de.pdf",
"circular_unica_de_auditores_externos.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20que%20establecen%20los%20requisitos%20que%20deber%C3%A1n%20cumplir%20los%20auditores%20y%20otros%20profesionales%20que.pdf", "circular_unica_de_auditores_externos.pdf": "https://www.cnbv.gob.mx/Normatividad/Disposiciones%20de%20car%C3%A1cter%20general%20que%20establecen%20los%20requisitos%20que%20deber%C3%A1n%20cumplir%20los%20auditores%20y%20otros%20profesionales%20que.pdf",
"ley_de_instituciones_de_Credito.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20de%20Instituciones%20de%20Cr%C3%A9dito.pdf", "ley_de_instituciones_de_Credito.pdf": "https://www.cnbv.gob.mx/Normatividad/Ley%20de%20Instituciones%20de%20Cr%C3%A9dito.pdf",
# Circulares de Banxico # Circulares de Banxico
"circular_13_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2007/cobro-intereses-por-adelantad.html", "circular_13_2007.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2007/cobro-intereses-por-adelantad.html",
"circular_13_2011.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2011/%7BBA4CBC28-A468-16C9-6F17-9EA9D7B03318%7D.pdf", "circular_13_2011.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-13-2011/%7BBA4CBC28-A468-16C9-6F17-9EA9D7B03318%7D.pdf",
@@ -55,15 +52,12 @@ PDF_PUBLIC_URLS = {
"circular_36_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-36-2010/%7B26C55DE6-CC3A-3368-34FC-1A6C50B11130%7D.pdf", "circular_36_2010.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-36-2010/%7B26C55DE6-CC3A-3368-34FC-1A6C50B11130%7D.pdf",
"circular_3_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-3-2012/%7B4E0281A4-7AD8-1462-BC79-7F2925F3171D%7D.pdf", "circular_3_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-3-2012/%7B4E0281A4-7AD8-1462-BC79-7F2925F3171D%7D.pdf",
"circular_4_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-4-2012/%7B97C62974-1C94-19AE-AB5A-D0D949A36247%7D.pdf", "circular_4_2012.pdf": "https://www.banxico.org.mx/marco-normativo/normativa-emitida-por-el-banco-de-mexico/circular-4-2012/%7B97C62974-1C94-19AE-AB5A-D0D949A36247%7D.pdf",
# CONDUSEF # CONDUSEF
"circular_unica_de_condusef.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf", "circular_unica_de_condusef.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
"ley_para_regular_las_sociedades_de_informacion_crediticia.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf", "ley_para_regular_las_sociedades_de_informacion_crediticia.pdf": "https://www.condusef.gob.mx/documentos/marco_legal/disposiciones-transparencia-if-sofom.pdf",
# Leyes federales # Leyes federales
"ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf", "ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/pdf/LFPDPPP.pdf",
"reglamento_de_la_ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/regley/Reg_LFPDPPP.pdf", "reglamento_de_la_ley_federal_de_proteccion_de_datos_personales_en_posesion_de_los_particulares.pdf": "https://www.diputados.gob.mx/LeyesBiblio/regley/Reg_LFPDPPP.pdf",
# SharePoint Banorte # SharePoint Banorte
"Modificaciones Recursos Procedencia Ilícita jul 25 PLD.pdf": "https://gfbanorte.sharepoint.com/:w:/r/sites/Formatosyplantillas/Documentos%20compartidos/Otros/Modificaciones%20Recursos%20Procedencia%20Il%C3%ADcita%20jul%2025%20PLD.docx?d=w6a941e9e2c26403ea41c12de35536516&csf=1&web=1&e=EHtc9b", "Modificaciones Recursos Procedencia Ilícita jul 25 PLD.pdf": "https://gfbanorte.sharepoint.com/:w:/r/sites/Formatosyplantillas/Documentos%20compartidos/Otros/Modificaciones%20Recursos%20Procedencia%20Il%C3%ADcita%20jul%2025%20PLD.docx?d=w6a941e9e2c26403ea41c12de35536516&csf=1&web=1&e=EHtc9b",
} }
@@ -79,17 +73,17 @@ app = FastAPI(lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["*"] expose_headers=["*"],
) )
agent = Agent() agent = Agent()
PDF_FOLDER = Path(__file__).parent / "agent" / "pdf" PDF_FOLDER = Path(__file__).parent / "agent" / "pdf"
PDF_FOLDER.mkdir(parents=True, exist_ok=True) PDF_FOLDER.mkdir(parents=True, exist_ok=True)
@app.post("/api/v1/conversation") @app.post("/api/v1/conversation")
@@ -110,21 +104,21 @@ async def send(message: Message):
trace = langfuse.trace( trace = langfuse.trace(
name="rag_chat", name="rag_chat",
session_id=str(message.conversation_id), session_id=str(message.conversation_id),
input={"prompt": message.prompt} input={"prompt": message.prompt},
) )
def b64_sse(func): def b64_sse(func):
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
response_parts = [] response_parts = []
async for chunk in func(*args, **kwargs): async for chunk in func(*args, **kwargs):
if chunk.type == "text" and chunk.content: if chunk.type == "text" and chunk.content:
response_parts.append(str(chunk.content)) response_parts.append(str(chunk.content))
content = chunk.model_dump_json() content = chunk.model_dump_json()
data = f"data: {content}\n\n" data = f"data: {content}\n\n"
yield data yield data
# Solo registrar input y output # Solo registrar input y output
full_response = "".join(response_parts) full_response = "".join(response_parts)
trace.update(output={"response": full_response}) trace.update(output={"response": full_response})
@@ -139,17 +133,21 @@ async def send(message: Message):
@app.get("/api/pdf/{filename}") @app.get("/api/pdf/{filename}")
async def get_pdf(filename: str): async def get_pdf(filename: str):
print(f"🔍 Solicitud PDF para: {filename}") print(f"🔍 Solicitud PDF para: {filename}")
if not filename.lower().endswith('.pdf'): if not filename.lower().endswith(".pdf"):
print(f"❌ Archivo no es PDF: {filename}") print(f"❌ Archivo no es PDF: {filename}")
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF") raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or ('/' in filename and not filename.startswith('http')) or '\\' in filename: if (
".." in filename
or ("/" in filename and not filename.startswith("http"))
or "\\" in filename
):
print(f"❌ Nombre de archivo inválido: {filename}") print(f"❌ Nombre de archivo inválido: {filename}")
raise HTTPException(status_code=400, detail="Nombre de archivo inválido") raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename) public_url = PDF_PUBLIC_URLS.get(filename)
if public_url: if public_url:
print(f"✅ Redirigiendo a URL pública: {public_url}") print(f"✅ Redirigiendo a URL pública: {public_url}")
return RedirectResponse( return RedirectResponse(
@@ -159,39 +157,41 @@ async def get_pdf(filename: str):
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*" "Access-Control-Allow-Headers": "*",
} },
) )
pdf_path = PDF_FOLDER / filename pdf_path = PDF_FOLDER / filename
if not pdf_path.exists(): if not pdf_path.exists():
print(f"❌ PDF no encontrado: {pdf_path}") print(f"❌ PDF no encontrado: {pdf_path}")
raise HTTPException(status_code=404, detail=f"PDF no encontrado. Archivo: {filename}") raise HTTPException(
status_code=404, detail=f"PDF no encontrado. Archivo: {filename}"
)
if not pdf_path.is_file(): if not pdf_path.is_file():
print(f"❌ No es un archivo: {pdf_path}") print(f"❌ No es un archivo: {pdf_path}")
raise HTTPException(status_code=404, detail="El recurso no es un archivo") raise HTTPException(status_code=404, detail="El recurso no es un archivo")
file_size = pdf_path.stat().st_size file_size = pdf_path.stat().st_size
print(f"📄 Sirviendo archivo local: {filename} ({file_size} bytes)") print(f"📄 Sirviendo archivo local: {filename} ({file_size} bytes)")
if file_size == 0: if file_size == 0:
print(f"❌ Archivo vacío: {pdf_path}") print(f"❌ Archivo vacío: {pdf_path}")
raise HTTPException(status_code=500, detail="El archivo PDF está vacío") raise HTTPException(status_code=500, detail="El archivo PDF está vacío")
return FileResponse( return FileResponse(
path=str(pdf_path), path=str(pdf_path),
media_type="application/pdf", media_type="application/pdf",
filename=filename, filename=filename,
headers={ headers={
"Content-Disposition": f"inline; filename={filename}", "Content-Disposition": f"inline; filename={filename}",
"Content-Type": "application/pdf", "Content-Type": "application/pdf",
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"X-Frame-Options": "ALLOWALL", "X-Frame-Options": "ALLOWALL",
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*" "Access-Control-Allow-Origin": "*",
} },
) )
@@ -199,29 +199,33 @@ async def get_pdf(filename: str):
async def list_pdfs(): async def list_pdfs():
try: try:
pdf_files = [] pdf_files = []
for filename, url in PDF_PUBLIC_URLS.items(): for filename, url in PDF_PUBLIC_URLS.items():
pdf_files.append({ pdf_files.append(
"filename": filename, {
"size": "N/A (Público)", "filename": filename,
"url": f"/api/pdf/{filename}", "size": "N/A (Público)",
"public_url": url, "url": f"/api/pdf/{filename}",
"type": "public" "public_url": url,
}) "type": "public",
}
)
local_files = [] local_files = []
for pattern in ["*.pdf", "*.PDF"]: for pattern in ["*.pdf", "*.PDF"]:
for file_path in PDF_FOLDER.glob(pattern): for file_path in PDF_FOLDER.glob(pattern):
if file_path.is_file() and file_path.name not in PDF_PUBLIC_URLS: if file_path.is_file() and file_path.name not in PDF_PUBLIC_URLS:
local_files.append({ local_files.append(
"filename": file_path.name, {
"size": file_path.stat().st_size, "filename": file_path.name,
"url": f"/api/pdf/{file_path.name}", "size": file_path.stat().st_size,
"type": "local" "url": f"/api/pdf/{file_path.name}",
}) "type": "local",
}
)
pdf_files.extend(local_files) pdf_files.extend(local_files)
debug_info = { debug_info = {
"current_working_directory": str(Path.cwd()), "current_working_directory": str(Path.cwd()),
"pdf_folder_path": str(PDF_FOLDER.absolute()), "pdf_folder_path": str(PDF_FOLDER.absolute()),
@@ -230,52 +234,49 @@ async def list_pdfs():
"local_files_count": len(local_files), "local_files_count": len(local_files),
"public_files": list(PDF_PUBLIC_URLS.keys()), "public_files": list(PDF_PUBLIC_URLS.keys()),
} }
return { return {"pdfs": pdf_files, "debug": debug_info, "total_pdfs": len(pdf_files)}
"pdfs": pdf_files,
"debug": debug_info,
"total_pdfs": len(pdf_files)
}
except Exception as e: except Exception as e:
import traceback import traceback
return { return {
"error": str(e), "error": str(e),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
"debug": { "debug": {
"current_working_directory": str(Path.cwd()), "current_working_directory": str(Path.cwd()),
"script_file_path": __file__ if '__file__' in globals() else "unknown" "script_file_path": __file__ if "__file__" in globals() else "unknown",
} },
} }
@app.get("/api/pdf/{filename}/info") @app.get("/api/pdf/{filename}/info")
async def get_pdf_info(filename: str): async def get_pdf_info(filename: str):
if not filename.lower().endswith('.pdf'): if not filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF") raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or '/' in filename or '\\' in filename: if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Nombre de archivo inválido") raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename) public_url = PDF_PUBLIC_URLS.get(filename)
if public_url: if public_url:
return { return {
"filename": filename, "filename": filename,
"size": "N/A", "size": "N/A",
"size_mb": "N/A", "size_mb": "N/A",
"modified": "N/A", "modified": "N/A",
"url": f"/api/pdf/{filename}", "url": f"/api/pdf/{filename}",
"public_url": public_url, "public_url": public_url,
"type": "public" "type": "public",
} }
pdf_path = PDF_FOLDER / filename pdf_path = PDF_FOLDER / filename
if not pdf_path.exists(): if not pdf_path.exists():
raise HTTPException(status_code=404, detail="PDF no encontrado") raise HTTPException(status_code=404, detail="PDF no encontrado")
if not pdf_path.is_file(): if not pdf_path.is_file():
raise HTTPException(status_code=404, detail="El recurso no es un archivo") raise HTTPException(status_code=404, detail="El recurso no es un archivo")
try: try:
file_stat = pdf_path.stat() file_stat = pdf_path.stat()
return { return {
@@ -284,10 +285,12 @@ async def get_pdf_info(filename: str):
"size_mb": round(file_stat.st_size / (1024 * 1024), 2), "size_mb": round(file_stat.st_size / (1024 * 1024), 2),
"modified": file_stat.st_mtime, "modified": file_stat.st_mtime,
"url": f"/api/pdf/{filename}", "url": f"/api/pdf/{filename}",
"type": "local" "type": "local",
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error al obtener información del PDF: {str(e)}") raise HTTPException(
status_code=500, detail=f"Error al obtener información del PDF: {str(e)}"
)
@app.get("/api/health") @app.get("/api/health")
@@ -296,5 +299,10 @@ async def health_check():
"status": "healthy", "status": "healthy",
"pdf_folder": str(PDF_FOLDER), "pdf_folder": str(PDF_FOLDER),
"pdf_folder_exists": PDF_FOLDER.exists(), "pdf_folder_exists": PDF_FOLDER.exists(),
"public_urls_configured": len(PDF_PUBLIC_URLS) "public_urls_configured": len(PDF_PUBLIC_URLS),
} }
@app.get("/")
async def health():
return {"status": "ok"}

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-pyme
namespace: apps
labels:
app: mayacontigo-pyme
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-pyme
template:
metadata:
labels:
app: mayacontigo-pyme
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-pyme
image: gitea.ia-innovacion.work/innovacion/mayacontigo-pyme:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-pyme-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-pyme-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-pyme.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-pyme-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-pyme-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-pyme-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-pyme
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-pyme-service
labels:
app: mayacontigo-pyme
spec:
selector:
app: mayacontigo-pyme
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -1,15 +1,14 @@
import uuid
import os import os
import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv # ← Agregar este import
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, FileResponse, RedirectResponse from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from pydantic import BaseModel
from langfuse import Langfuse from langfuse import Langfuse
from pydantic import BaseModel
from dotenv import load_dotenv # ← Agregar este import
from api import services from api import services
from api.agent import Agent from api.agent import Agent
@@ -21,14 +20,13 @@ load_dotenv()
# Configurar Langfuse desde variables de entorno # Configurar Langfuse desde variables de entorno
langfuse = Langfuse( langfuse = Langfuse(
public_key=os.getenv("LANGFUSE_PUBLIC_KEY"), public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
secret_key=os.getenv("LANGFUSE_SECRET_KEY"), secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
host=os.getenv("LANGFUSE_HOST") host=os.getenv("LANGFUSE_HOST"),
) )
# Mapeo completo de archivos a URLs públicas # Mapeo completo de archivos a URLs públicas
PDF_PUBLIC_URLS = { PDF_PUBLIC_URLS = {}
}
@asynccontextmanager @asynccontextmanager
@@ -41,17 +39,17 @@ app = FastAPI(lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=["*"], allow_origins=["*"],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["*"] expose_headers=["*"],
) )
agent = Agent() agent = Agent()
PDF_FOLDER = Path(__file__).parent / "agent" / "pdf" PDF_FOLDER = Path(__file__).parent / "agent" / "pdf"
PDF_FOLDER.mkdir(parents=True, exist_ok=True) PDF_FOLDER.mkdir(parents=True, exist_ok=True)
@app.post("/api/v1/conversation") @app.post("/api/v1/conversation")
@@ -72,21 +70,21 @@ async def send(message: Message):
trace = langfuse.trace( trace = langfuse.trace(
name="rag_chat", name="rag_chat",
session_id=str(message.conversation_id), session_id=str(message.conversation_id),
input={"prompt": message.prompt} input={"prompt": message.prompt},
) )
def b64_sse(func): def b64_sse(func):
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
response_parts = [] response_parts = []
async for chunk in func(*args, **kwargs): async for chunk in func(*args, **kwargs):
if chunk.type == "text" and chunk.content: if chunk.type == "text" and chunk.content:
response_parts.append(str(chunk.content)) response_parts.append(str(chunk.content))
content = chunk.model_dump_json() content = chunk.model_dump_json()
data = f"data: {content}\n\n" data = f"data: {content}\n\n"
yield data yield data
# Solo registrar input y output # Solo registrar input y output
full_response = "".join(response_parts) full_response = "".join(response_parts)
trace.update(output={"response": full_response}) trace.update(output={"response": full_response})
@@ -101,17 +99,21 @@ async def send(message: Message):
@app.get("/api/pdf/{filename}") @app.get("/api/pdf/{filename}")
async def get_pdf(filename: str): async def get_pdf(filename: str):
print(f"🔍 Solicitud PDF para: {filename}") print(f"🔍 Solicitud PDF para: {filename}")
if not filename.lower().endswith('.pdf'): if not filename.lower().endswith(".pdf"):
print(f"❌ Archivo no es PDF: {filename}") print(f"❌ Archivo no es PDF: {filename}")
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF") raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or ('/' in filename and not filename.startswith('http')) or '\\' in filename: if (
".." in filename
or ("/" in filename and not filename.startswith("http"))
or "\\" in filename
):
print(f"❌ Nombre de archivo inválido: {filename}") print(f"❌ Nombre de archivo inválido: {filename}")
raise HTTPException(status_code=400, detail="Nombre de archivo inválido") raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename) public_url = PDF_PUBLIC_URLS.get(filename)
if public_url: if public_url:
print(f"✅ Redirigiendo a URL pública: {public_url}") print(f"✅ Redirigiendo a URL pública: {public_url}")
return RedirectResponse( return RedirectResponse(
@@ -121,39 +123,41 @@ async def get_pdf(filename: str):
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*" "Access-Control-Allow-Headers": "*",
} },
) )
pdf_path = PDF_FOLDER / filename pdf_path = PDF_FOLDER / filename
if not pdf_path.exists(): if not pdf_path.exists():
print(f"❌ PDF no encontrado: {pdf_path}") print(f"❌ PDF no encontrado: {pdf_path}")
raise HTTPException(status_code=404, detail=f"PDF no encontrado. Archivo: {filename}") raise HTTPException(
status_code=404, detail=f"PDF no encontrado. Archivo: {filename}"
)
if not pdf_path.is_file(): if not pdf_path.is_file():
print(f"❌ No es un archivo: {pdf_path}") print(f"❌ No es un archivo: {pdf_path}")
raise HTTPException(status_code=404, detail="El recurso no es un archivo") raise HTTPException(status_code=404, detail="El recurso no es un archivo")
file_size = pdf_path.stat().st_size file_size = pdf_path.stat().st_size
print(f"📄 Sirviendo archivo local: {filename} ({file_size} bytes)") print(f"📄 Sirviendo archivo local: {filename} ({file_size} bytes)")
if file_size == 0: if file_size == 0:
print(f"❌ Archivo vacío: {pdf_path}") print(f"❌ Archivo vacío: {pdf_path}")
raise HTTPException(status_code=500, detail="El archivo PDF está vacío") raise HTTPException(status_code=500, detail="El archivo PDF está vacío")
return FileResponse( return FileResponse(
path=str(pdf_path), path=str(pdf_path),
media_type="application/pdf", media_type="application/pdf",
filename=filename, filename=filename,
headers={ headers={
"Content-Disposition": f"inline; filename={filename}", "Content-Disposition": f"inline; filename={filename}",
"Content-Type": "application/pdf", "Content-Type": "application/pdf",
"Cache-Control": "public, max-age=3600", "Cache-Control": "public, max-age=3600",
"X-Frame-Options": "ALLOWALL", "X-Frame-Options": "ALLOWALL",
"X-Content-Type-Options": "nosniff", "X-Content-Type-Options": "nosniff",
"Access-Control-Allow-Origin": "*" "Access-Control-Allow-Origin": "*",
} },
) )
@@ -161,29 +165,33 @@ async def get_pdf(filename: str):
async def list_pdfs(): async def list_pdfs():
try: try:
pdf_files = [] pdf_files = []
for filename, url in PDF_PUBLIC_URLS.items(): for filename, url in PDF_PUBLIC_URLS.items():
pdf_files.append({ pdf_files.append(
"filename": filename, {
"size": "N/A (Público)", "filename": filename,
"url": f"/api/pdf/{filename}", "size": "N/A (Público)",
"public_url": url, "url": f"/api/pdf/{filename}",
"type": "public" "public_url": url,
}) "type": "public",
}
)
local_files = [] local_files = []
for pattern in ["*.pdf", "*.PDF"]: for pattern in ["*.pdf", "*.PDF"]:
for file_path in PDF_FOLDER.glob(pattern): for file_path in PDF_FOLDER.glob(pattern):
if file_path.is_file() and file_path.name not in PDF_PUBLIC_URLS: if file_path.is_file() and file_path.name not in PDF_PUBLIC_URLS:
local_files.append({ local_files.append(
"filename": file_path.name, {
"size": file_path.stat().st_size, "filename": file_path.name,
"url": f"/api/pdf/{file_path.name}", "size": file_path.stat().st_size,
"type": "local" "url": f"/api/pdf/{file_path.name}",
}) "type": "local",
}
)
pdf_files.extend(local_files) pdf_files.extend(local_files)
debug_info = { debug_info = {
"current_working_directory": str(Path.cwd()), "current_working_directory": str(Path.cwd()),
"pdf_folder_path": str(PDF_FOLDER.absolute()), "pdf_folder_path": str(PDF_FOLDER.absolute()),
@@ -192,52 +200,49 @@ async def list_pdfs():
"local_files_count": len(local_files), "local_files_count": len(local_files),
"public_files": list(PDF_PUBLIC_URLS.keys()), "public_files": list(PDF_PUBLIC_URLS.keys()),
} }
return { return {"pdfs": pdf_files, "debug": debug_info, "total_pdfs": len(pdf_files)}
"pdfs": pdf_files,
"debug": debug_info,
"total_pdfs": len(pdf_files)
}
except Exception as e: except Exception as e:
import traceback import traceback
return { return {
"error": str(e), "error": str(e),
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
"debug": { "debug": {
"current_working_directory": str(Path.cwd()), "current_working_directory": str(Path.cwd()),
"script_file_path": __file__ if '__file__' in globals() else "unknown" "script_file_path": __file__ if "__file__" in globals() else "unknown",
} },
} }
@app.get("/api/pdf/{filename}/info") @app.get("/api/pdf/{filename}/info")
async def get_pdf_info(filename: str): async def get_pdf_info(filename: str):
if not filename.lower().endswith('.pdf'): if not filename.lower().endswith(".pdf"):
raise HTTPException(status_code=400, detail="El archivo debe ser un PDF") raise HTTPException(status_code=400, detail="El archivo debe ser un PDF")
if '..' in filename or '/' in filename or '\\' in filename: if ".." in filename or "/" in filename or "\\" in filename:
raise HTTPException(status_code=400, detail="Nombre de archivo inválido") raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
public_url = PDF_PUBLIC_URLS.get(filename) public_url = PDF_PUBLIC_URLS.get(filename)
if public_url: if public_url:
return { return {
"filename": filename, "filename": filename,
"size": "N/A", "size": "N/A",
"size_mb": "N/A", "size_mb": "N/A",
"modified": "N/A", "modified": "N/A",
"url": f"/api/pdf/{filename}", "url": f"/api/pdf/{filename}",
"public_url": public_url, "public_url": public_url,
"type": "public" "type": "public",
} }
pdf_path = PDF_FOLDER / filename pdf_path = PDF_FOLDER / filename
if not pdf_path.exists(): if not pdf_path.exists():
raise HTTPException(status_code=404, detail="PDF no encontrado") raise HTTPException(status_code=404, detail="PDF no encontrado")
if not pdf_path.is_file(): if not pdf_path.is_file():
raise HTTPException(status_code=404, detail="El recurso no es un archivo") raise HTTPException(status_code=404, detail="El recurso no es un archivo")
try: try:
file_stat = pdf_path.stat() file_stat = pdf_path.stat()
return { return {
@@ -246,10 +251,12 @@ async def get_pdf_info(filename: str):
"size_mb": round(file_stat.st_size / (1024 * 1024), 2), "size_mb": round(file_stat.st_size / (1024 * 1024), 2),
"modified": file_stat.st_mtime, "modified": file_stat.st_mtime,
"url": f"/api/pdf/{filename}", "url": f"/api/pdf/{filename}",
"type": "local" "type": "local",
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Error al obtener información del PDF: {str(e)}") raise HTTPException(
status_code=500, detail=f"Error al obtener información del PDF: {str(e)}"
)
@app.get("/api/health") @app.get("/api/health")
@@ -258,5 +265,10 @@ async def health_check():
"status": "healthy", "status": "healthy",
"pdf_folder": str(PDF_FOLDER), "pdf_folder": str(PDF_FOLDER),
"pdf_folder_exists": PDF_FOLDER.exists(), "pdf_folder_exists": PDF_FOLDER.exists(),
"public_urls_configured": len(PDF_PUBLIC_URLS) "public_urls_configured": len(PDF_PUBLIC_URLS),
} }
@app.get("/")
async def health():
return {"app": "RAG PyME", "status": "OK"}

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-riesgos
namespace: apps
labels:
app: mayacontigo-riesgos
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-riesgos
template:
metadata:
labels:
app: mayacontigo-riesgos
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-riesgos
image: gitea.ia-innovacion.work/innovacion/mayacontigo-riesgos:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-riesgos-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-riesgos-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-riesgos.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-riesgos-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-riesgos-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-riesgos-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-riesgos
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-riesgos-service
labels:
app: mayacontigo-riesgos
spec:
selector:
app: mayacontigo-riesgos
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -20,4 +20,10 @@ app = FastAPI(
openapi_url="/api/openapi.json", openapi_url="/api/openapi.json",
) )
@app.get("/")
async def health():
return {"status": "ok"}
app.include_router(router) app.include_router(router)

View File

@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: mayacontigo-voz-del-cliente
namespace: apps
labels:
app: mayacontigo-voz-del-cliente
spec:
replicas: 1
selector:
matchLabels:
app: mayacontigo-voz-del-cliente
template:
metadata:
labels:
app: mayacontigo-voz-del-cliente
spec:
imagePullSecrets:
- name: gitea-registry-cred
containers:
- name: mayacontigo-voz-del-cliente
image: gitea.ia-innovacion.work/innovacion/mayacontigo-voz-del-cliente:latest
env:
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: mayacontigo-voz-del-cliente-secret
key: VAULT_TOKEN
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20

View File

@@ -0,0 +1,19 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mayacontigo-voz-del-cliente-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: mayacontigo-voz-del-cliente.app.ia-innovacion.work
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mayacontigo-voz-del-cliente-service
port:
number: 80

View File

@@ -0,0 +1,17 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mayacontigo-voz-del-cliente-vault
namespace: apps
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: mayacontigo-voz-del-cliente-secret
data:
- secretKey: VAULT_TOKEN
remoteRef:
key: mayacontigo-voz-del-cliente
property: VAULT_TOKEN

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: mayacontigo-voz-del-cliente-service
labels:
app: mayacontigo-voz-del-cliente
spec:
selector:
app: mayacontigo-voz-del-cliente
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP

View File

@@ -1,9 +1,7 @@
from hvac import Client from hvac import Client
from pydantic import Field from pydantic import Field
from dotenv import load_dotenv
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
client = Client(url="https://vault.ia-innovacion.work") client = Client(url="https://vault.ia-innovacion.work")
if not client.is_authenticated(): if not client.is_authenticated():
@@ -14,6 +12,7 @@ secret_map = client.secrets.kv.v2.read_secret_version(
)["data"]["data"] )["data"]["data"]
class Settings(BaseSettings): class Settings(BaseSettings):
""" """
Esta clase obtiene sus valores de variables de ambiente. Esta clase obtiene sus valores de variables de ambiente.
Si no estan en el ambiente, los jala de nuestra Vault. Si no estan en el ambiente, los jala de nuestra Vault.
@@ -39,11 +38,10 @@ class Settings(BaseSettings):
async def init_mongo_db(self): async def init_mongo_db(self):
"""Este helper inicia la conexion enter el MongoDB ORM y nuestra instancia""" """Este helper inicia la conexion enter el MongoDB ORM y nuestra instancia"""
from banortegpt.database.mongo_memory.models import Conversation
from beanie import init_beanie from beanie import init_beanie
from motor.motor_asyncio import AsyncIOMotorClient from motor.motor_asyncio import AsyncIOMotorClient
from banortegpt.database.mongo_memory.models import Conversation
await init_beanie( await init_beanie(
database=AsyncIOMotorClient(self.mongodb_url).voz_del_cliente, database=AsyncIOMotorClient(self.mongodb_url).voz_del_cliente,
document_models=[Conversation], document_models=[Conversation],

View File

@@ -35,16 +35,24 @@ class Message(BaseModel):
@app.post("/api/v1/message") @app.post("/api/v1/message")
async def send(message: Message): async def send(message: Message):
def b64_sse(func): def b64_sse(func):
"""Este helper transforma un generador de strings a un generador del protocolo SSE""" """Este helper transforma un generador de strings a un generador del protocolo SSE"""
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
async for chunk in func(*args, **kwargs): async for chunk in func(*args, **kwargs):
content = chunk.model_dump_json() content = chunk.model_dump_json()
data = f"data: {content}\n\n" data = f"data: {content}\n\n"
yield data yield data
return wrapper return wrapper
sse_stream = b64_sse(services.stream) sse_stream = b64_sse(services.stream)
generator = sse_stream(agent, message.prompt, message.conversation_id, message.with_deep_research) generator = sse_stream(
agent, message.prompt, message.conversation_id, message.with_deep_research
)
return StreamingResponse(generator, media_type="text/event-stream") return StreamingResponse(generator, media_type="text/event-stream")
@app.get("/")
async def health():
return {"app": "Voz del Cliente", "status": "OK"}

View File

@@ -45,16 +45,16 @@ services:
- traefik.http.routers.ocp.entrypoints=web - traefik.http.routers.ocp.entrypoints=web
- traefik.http.routers.ocp.middlewares=ocp-strip - traefik.http.routers.ocp.middlewares=ocp-strip
- traefik.http.middlewares.ocp-strip.stripprefix.prefixes=/api/mayaocp - traefik.http.middlewares.ocp-strip.stripprefix.prefixes=/api/mayaocp
ChatEgresos: egresos:
image: mayacontigo/chategresos:latest image: mayacontigo/egresos:latest
build: build:
context: . context: .
dockerfile: .containers/unit/Dockerfile dockerfile: .containers/unit/Dockerfile
args: args:
PACKAGE: ChatEgresos PACKAGE: egresos
x-bake: x-bake:
tags: tags:
- mayacontigo/chategresos:latest - mayacontigo/egresos:latest
ports: ports:
- 8001:80 - 8001:80
labels: labels:
@@ -69,7 +69,7 @@ services:
context: . context: .
dockerfile: .containers/unit/Dockerfile dockerfile: .containers/unit/Dockerfile
args: args:
PACKAGE: nnormativa PACKAGE: voz-del-cliente
x-bake: x-bake:
tags: tags:
- mayacontigo/nnormativa:latest - mayacontigo/nnormativa:latest
@@ -99,42 +99,6 @@ services:
- traefik.http.routers.normativa.entrypoints=web - traefik.http.routers.normativa.entrypoints=web
- traefik.http.routers.normativa.middlewares=normativa-strip - traefik.http.routers.normativa.middlewares=normativa-strip
- traefik.http.middlewares.normativa-strip.stripprefix.prefixes=/api/mayanormativa - traefik.http.middlewares.normativa-strip.stripprefix.prefixes=/api/mayanormativa
Test:
image: mayacontigo/Test:latest
build:
context: .
dockerfile: .containers/unit/Dockerfile
args:
PACKAGE: Test
x-bake:
tags:
- mayacontigo/Test:latest
ports:
- 8002:80
labels:
- traefik.enable=true
- traefik.http.routers.Test.rule=PathPrefix(`/api/mayaTest`)
- traefik.http.routers.Test.entrypoints=web
- traefik.http.routers.Test.middlewares=Test-strip
- traefik.http.middlewares.Test-strip.stripprefix.prefixes=/api/mayaTest
nnormativa:
image: mayacontigo/nnormativa:latest
build:
context: .
dockerfile: .containers/unit/Dockerfile
args:
PACKAGE: nnormativa
x-bake:
tags:
- mayacontigo/nnormativa:latest
ports:
- 8003:80
labels:
- traefik.enable=true
- traefik.http.routers.nnormativa.rule=PathPrefix(`/api/mayannormativa`)
- traefik.http.routers.nnormativa.entrypoints=web
- traefik.http.routers.nnormativa.middlewares=nnormativa-strip
- traefik.http.middlewares.nnormativa-strip.stripprefix.prefixes=/api/mayannormativa
pyme: pyme:
image: mayacontigo/pyme:latest image: mayacontigo/pyme:latest
build: build:

30
scripts/replace-app-name.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Replace app name in Kubernetes manifest files
# Usage: ./replace-app-name.sh <app_name> <old_name>
# Example: ./replace-app-name.sh bursatil inversionistas
if [ $# -ne 2 ]; then
echo "Usage: $0 <app_name> <old_name>"
echo "Example: $0 bursatil inversionistas"
exit 1
fi
APP_NAME=$1
OLD_NAME=$2
K8S_DIR="apps/$APP_NAME/.k8s"
if [ ! -d "$K8S_DIR" ]; then
echo "Error: Directory $K8S_DIR does not exist"
exit 1
fi
echo "Replacing '$OLD_NAME' with '$APP_NAME' in $K8S_DIR"
for file in "$K8S_DIR"/*.yaml; do
if [ -f "$file" ]; then
echo " Processing: $(basename $file)"
sed -i "s/$OLD_NAME/$APP_NAME/g" "$file"
fi
done
echo "Done!"

82
uv.lock generated
View File

@@ -13,8 +13,8 @@ members = [
"azure-storage", "azure-storage",
"banortegpt", "banortegpt",
"bursatil", "bursatil",
"chategresos",
"chunk-with-llm", "chunk-with-llm",
"egresos",
"google-storage", "google-storage",
"inversionistas", "inversionistas",
"mongo-memory", "mongo-memory",
@@ -27,7 +27,6 @@ members = [
"riesgos", "riesgos",
"search-evaluator", "search-evaluator",
"synthetic-question-generator", "synthetic-question-generator",
"test",
"vector-db-migrator", "vector-db-migrator",
"vertex-ai-gemini", "vertex-ai-gemini",
"voz-del-cliente", "voz-del-cliente",
@@ -503,33 +502,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
] ]
[[package]]
name = "chategresos"
version = "0.1.0"
source = { virtual = "apps/ChatEgresos" }
dependencies = [
{ name = "aiohttp" },
{ name = "fastapi" },
{ name = "hvac" },
{ name = "langchain-azure-ai", extra = ["opentelemetry"] },
{ name = "mongo-memory" },
{ name = "pydantic-settings" },
{ name = "qdrant" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = ">=3.11.16" },
{ name = "fastapi", specifier = ">=0.115.6" },
{ name = "hvac", specifier = ">=2.3.0" },
{ name = "langchain-azure-ai", extras = ["opentelemetry"], specifier = ">=0.1.4" },
{ name = "mongo-memory", editable = "packages/mongo-memory" },
{ name = "pydantic-settings", specifier = ">=2.8.1" },
{ name = "qdrant", editable = "packages/qdrant" },
{ name = "uvicorn", specifier = ">=0.34.0" },
]
[[package]] [[package]]
name = "chunk-with-llm" name = "chunk-with-llm"
version = "0.1.0" version = "0.1.0"
@@ -749,6 +721,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" },
] ]
[[package]]
name = "egresos"
version = "0.1.0"
source = { virtual = "apps/egresos" }
dependencies = [
{ name = "aiohttp" },
{ name = "fastapi" },
{ name = "hvac" },
{ name = "langchain-azure-ai", extra = ["opentelemetry"] },
{ name = "mongo-memory" },
{ name = "pydantic-settings" },
{ name = "qdrant" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = ">=3.11.16" },
{ name = "fastapi", specifier = ">=0.115.6" },
{ name = "hvac", specifier = ">=2.3.0" },
{ name = "langchain-azure-ai", extras = ["opentelemetry"], specifier = ">=0.1.4" },
{ name = "mongo-memory", editable = "packages/mongo-memory" },
{ name = "pydantic-settings", specifier = ">=2.8.1" },
{ name = "qdrant", editable = "packages/qdrant" },
{ name = "uvicorn", specifier = ">=0.34.0" },
]
[[package]] [[package]]
name = "email-validator" name = "email-validator"
version = "2.2.0" version = "2.2.0"
@@ -3438,31 +3437,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
] ]
[[package]]
name = "test"
version = "0.1.0"
source = { virtual = "apps/Test" }
dependencies = [
{ name = "aiohttp" },
{ name = "fastapi" },
{ name = "hvac" },
{ name = "langchain-azure-ai", extra = ["opentelemetry"] },
{ name = "mongo-memory" },
{ name = "pydantic-settings" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = ">=3.11.16" },
{ name = "fastapi", specifier = ">=0.115.6" },
{ name = "hvac", specifier = ">=2.3.0" },
{ name = "langchain-azure-ai", extras = ["opentelemetry"], specifier = ">=0.1.4" },
{ name = "mongo-memory", editable = "packages/mongo-memory" },
{ name = "pydantic-settings", specifier = ">=2.8.1" },
{ name = "uvicorn", specifier = ">=0.34.0" },
]
[[package]] [[package]]
name = "tiktoken" name = "tiktoken"
version = "0.9.0" version = "0.9.0"