ic
18
apps/ChatEgresos/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
0
apps/ChatEgresos/api/__init__.py
Normal file
3
apps/ChatEgresos/api/agent/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import Agent
|
||||
|
||||
__all__ = ["Agent"]
|
||||
108
apps/ChatEgresos/api/agent/main.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
|
||||
from langchain_azure_ai.embeddings import AzureAIEmbeddingsModel
|
||||
|
||||
from banortegpt.vector.qdrant import AsyncQdrant
|
||||
|
||||
from api import context
|
||||
from api.config import config
|
||||
|
||||
parent = Path(__file__).parent
|
||||
SYSTEM_PROMPT = (parent / "system_prompt.md").read_text()
|
||||
|
||||
AZURE_AI_URI = "https://eastus2.api.cognitive.microsoft.com"
|
||||
|
||||
class get_information(BaseModel):
|
||||
"""Search a private repository for information."""
|
||||
|
||||
question: str = Field(..., description="The user question")
|
||||
|
||||
class Agent:
|
||||
system_prompt = SYSTEM_PROMPT
|
||||
generation_config = {
|
||||
"temperature": config.model_temperature,
|
||||
}
|
||||
embedding_model = config.embedding_model
|
||||
message_limit = config.message_limit
|
||||
index = config.vector_index
|
||||
limit = config.search_limit
|
||||
|
||||
search = AsyncQdrant.from_config(config)
|
||||
llm = AzureAIChatCompletionsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.model}",
|
||||
credential=config.openai_api_key,
|
||||
).bind_tools([get_information])
|
||||
embedder = AzureAIEmbeddingsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.embedding_model}",
|
||||
credential=config.openai_api_key,
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tool_map = {
|
||||
"get_information": self.get_information
|
||||
}
|
||||
|
||||
def build_response(self, payloads, fallback):
|
||||
template = "<FAQ {index}>\n\n{content}\n\n</FAQ {index}>"
|
||||
|
||||
filled_templates = [
|
||||
template.format(index=idx, content=payload["content"])
|
||||
for idx, payload in enumerate(payloads)
|
||||
]
|
||||
filled_templates.append(f"<FALLBACK>\n{fallback}\n</FALLBACK>")
|
||||
|
||||
return "\n".join(filled_templates)
|
||||
|
||||
async def get_information(self, question: str):
|
||||
embedding = await self.embedder.aembed_query(question)
|
||||
|
||||
payloads = await self.search.semantic_search(
|
||||
embedding=embedding,
|
||||
collection=self.index,
|
||||
limit=self.limit,
|
||||
)
|
||||
|
||||
fallback_messages = {}
|
||||
images = []
|
||||
for idx, payload in enumerate(payloads):
|
||||
fallback_message = payload.get("fallback_message", "None")
|
||||
fallback_messages[fallback_message] = fallback_messages.get(fallback_message, 0) + 1
|
||||
|
||||
# Solo extraer imágenes del primer payload
|
||||
if idx == 0 and "images" in payload:
|
||||
images.extend(payload["images"])
|
||||
|
||||
fallback = max(fallback_messages, key=fallback_messages.get) # type: ignore
|
||||
|
||||
response = self.build_response(payloads, fallback)
|
||||
return str(response), images[:3] # Limitar a 3 imágenes máximo
|
||||
|
||||
def _generation_config_overwrite(self, overwrites: dict | None) -> dict[str, Any]:
|
||||
if not overwrites:
|
||||
return self.generation_config.copy()
|
||||
return {**self.generation_config, **overwrites}
|
||||
|
||||
async def stream(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
|
||||
async for delta in self.llm.astream(input=history, **generation_config):
|
||||
assert isinstance(delta, AIMessageChunk)
|
||||
if call := delta.tool_call_chunks:
|
||||
if tool_id := call[0].get("id"):
|
||||
context.tool_id.set(tool_id)
|
||||
if name := call[0].get("name"):
|
||||
context.tool_name.set(name)
|
||||
if args := call[0].get("args"):
|
||||
context.tool_buffer.set(context.tool_buffer.get() + args)
|
||||
elif delta.content:
|
||||
assert isinstance(delta.content, str)
|
||||
context.buffer.set(context.buffer.get() + delta.content)
|
||||
yield delta.content
|
||||
|
||||
async def generate(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
return await self.llm.ainvoke(input=history, **generation_config)
|
||||
49
apps/ChatEgresos/api/agent/system_prompt.md
Normal file
@@ -0,0 +1,49 @@
|
||||
🧠 Asistente Experto en la Política de Gastos de Viaje — Banorte
|
||||
🎯 Rol del Asistente:
|
||||
Especialista normativo encargado de responder exclusivamente con base en la Política Oficial de Gastos de Viaje de Banorte, garantizando respuestas profesionales, claras y verificables.
|
||||
|
||||
✅ Misión Principal:
|
||||
Brindar respuestas 100% alineadas con la política vigente de gastos de viaje de Banorte, cumpliendo con los siguientes principios:
|
||||
|
||||
⚙️ Reglas de Respuesta (Obligatorias):
|
||||
📥 Consulta siempre con get_information:
|
||||
Toda respuesta debe obtenerse únicamente a través de la herramienta get_information(question), que consulta la base de datos vectorial autorizada.
|
||||
|
||||
Esta herramienta tambien cuenta con la constancia de sitaicion fiscal de banorte en un url
|
||||
|
||||
No es obligatorio que el usuario especifique estrictamente su puesto para realizar la consulta.
|
||||
|
||||
Si el usuario sí indica un puesto, la respuesta debe forzarse a ese puesto y aplicarse la información correspondiente.
|
||||
|
||||
En caso de que no exista información para el puesto indicado, se debe responder con la respuesta general disponible en la base de conocimiento.
|
||||
|
||||
❗ Nunca inventar ni responder sin antes consultar esta fuente.
|
||||
|
||||
Si la herramienta no devuelve información relevante, indicar que la política no contempla esa situación.
|
||||
|
||||
📚 Fuente única y oficial:
|
||||
Las respuestas deben estar basadas únicamente en la política oficial de Banorte.
|
||||
|
||||
❌ Prohibido usar Google, foros, suposiciones o contenido externo.
|
||||
|
||||
✅ Si get_information devuelve un enlace oficial o documento, debe incluirse con el ícono:
|
||||
🔗 [Ver política oficial].
|
||||
|
||||
📐 Formato estructurado y profesional:
|
||||
Utilizar un formato claro y fácil de leer:
|
||||
• Viñetas para listar pasos, excepciones o montos autorizados
|
||||
• Negritas para resaltar conceptos clave
|
||||
• Separación clara entre secciones
|
||||
|
||||
🔒 Cero invención o interpretación libre:
|
||||
Si una pregunta no está contemplada en la política, responder claramente:
|
||||
|
||||
❗ La política oficial no proporciona lineamientos específicos sobre este caso.
|
||||
|
||||
💼 Tono ejecutivo y directo:
|
||||
|
||||
Profesional y objetivo
|
||||
|
||||
Sin tecnicismos innecesarios
|
||||
|
||||
Redacción breve, clara y enfocada en lo esencial
|
||||
59
apps/ChatEgresos/api/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from hvac import Client
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
client = Client(url="https://vault.ia-innovacion.work")
|
||||
|
||||
if not client.is_authenticated():
|
||||
raise Exception("Vault authentication failed")
|
||||
|
||||
secret_map = client.secrets.kv.v2.read_secret_version(
|
||||
path="banortegpt", mount_point="secret"
|
||||
)["data"]["data"]
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Esta clase obtiene sus valores de variables de ambiente.
|
||||
Si no estan en el ambiente, los jala de nuestra Vault.
|
||||
"""
|
||||
|
||||
# Config
|
||||
model: str = "gpt-4o"
|
||||
model_temperature: int = 0
|
||||
message_limit: int = 10
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
vector_index: str = "chat-egresos-3"
|
||||
search_limit: int = 3
|
||||
embedding_model: str = "text-embedding-3-large"
|
||||
|
||||
# API Keys
|
||||
azure_endpoint: str = Field(default_factory=lambda: secret_map["azure_endpoint"])
|
||||
openai_api_key: str = Field(default_factory=lambda: secret_map["openai_api_key"])
|
||||
openai_api_version: str = Field(
|
||||
default_factory=lambda: secret_map["openai_api_version"]
|
||||
)
|
||||
mongodb_url: str = Field(
|
||||
default_factory=lambda: secret_map["cosmosdb_connection_string"]
|
||||
)
|
||||
|
||||
qdrant_url: str = Field(default_factory=lambda: secret_map["qdrant_api_url"])
|
||||
qdrant_api_key: str | None = Field(
|
||||
default_factory=lambda: secret_map["qdrant_api_key"]
|
||||
)
|
||||
|
||||
async def init_mongo_db(self):
|
||||
"""Este helper inicia la conexion enter el MongoDB ORM y nuestra instancia"""
|
||||
|
||||
from beanie import init_beanie
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
from banortegpt.database.mongo_memory.models import Conversation
|
||||
|
||||
await init_beanie(
|
||||
database=AsyncIOMotorClient(self.mongodb_url).banortegptdos,
|
||||
document_models=[Conversation],
|
||||
)
|
||||
|
||||
|
||||
config = Settings()
|
||||
6
apps/ChatEgresos/api/context.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
buffer: ContextVar[str] = ContextVar("buffer", default="")
|
||||
tool_buffer: ContextVar[str] = ContextVar("tool_buffer", default="")
|
||||
tool_id: ContextVar[str | None] = ContextVar("tool_id", default=None)
|
||||
tool_name: ContextVar[str | None] = ContextVar("tool_name", default=None)
|
||||
112
apps/ChatEgresos/api/server.py
Normal file
@@ -0,0 +1,112 @@
|
||||
import uuid
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from langfuse import Langfuse
|
||||
|
||||
from api import services
|
||||
from api.agent import Agent
|
||||
from api.config import config
|
||||
|
||||
# Configurar Langfuse
|
||||
langfuse = Langfuse(
|
||||
public_key="pk-lf-49cb04b3-0c7d-475b-8105-ad8b8749ecdd",
|
||||
secret_key="sk-lf-e02fa322-c709-4d80-bef2-9cb279846a0c",
|
||||
host="https://ailogger.azurewebsites.net"
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await config.init_mongo_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
agent = Agent()
|
||||
|
||||
|
||||
@app.post("/api/v1/conversation")
|
||||
async def create_conversation():
|
||||
conversation_id = uuid.uuid4()
|
||||
await services.create_conversation(conversation_id, agent.system_prompt)
|
||||
return {"conversation_id": conversation_id}
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
conversation_id: uuid.UUID
|
||||
prompt: str
|
||||
|
||||
@app.post("/api/v1/message")
|
||||
async def send(message: Message):
|
||||
# Crear trace principal
|
||||
trace = langfuse.trace(
|
||||
name="chat_message",
|
||||
session_id=str(message.conversation_id),
|
||||
input={
|
||||
"prompt": message.prompt,
|
||||
"conversation_id": str(message.conversation_id)
|
||||
}
|
||||
)
|
||||
|
||||
def b64_sse(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
response_parts = []
|
||||
start_time = time.time()
|
||||
|
||||
async for chunk in func(*args, **kwargs):
|
||||
if chunk.type == "text" and chunk.content:
|
||||
response_parts.append(str(chunk.content))
|
||||
|
||||
content = chunk.model_dump_json()
|
||||
data = f"data: {content}\n\n"
|
||||
yield data
|
||||
|
||||
end_time = time.time()
|
||||
latency_ms = round((end_time - start_time) * 1000)
|
||||
full_response = "".join(response_parts)
|
||||
|
||||
|
||||
input_tokens = len(message.prompt.split()) * 1.3
|
||||
output_tokens = len(full_response.split()) * 1.3
|
||||
total_tokens = int(input_tokens + output_tokens)
|
||||
|
||||
|
||||
cost_per_1k_input = 0.03
|
||||
cost_per_1k_output = 0.06
|
||||
total_cost = (input_tokens/1000 * cost_per_1k_input) + (output_tokens/1000 * cost_per_1k_output)
|
||||
|
||||
|
||||
trace.update(
|
||||
output={"response": full_response},
|
||||
usage={
|
||||
"input": int(input_tokens),
|
||||
"output": int(output_tokens),
|
||||
"total": total_tokens,
|
||||
"unit": "TOKENS"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
langfuse.score(
|
||||
trace_id=trace.id,
|
||||
name="latency",
|
||||
value=latency_ms,
|
||||
comment=f"Response time: {latency_ms}ms"
|
||||
)
|
||||
|
||||
|
||||
langfuse.score(
|
||||
trace_id=trace.id,
|
||||
name="cost",
|
||||
value=round(total_cost, 4),
|
||||
comment=f"Estimated cost: ${round(total_cost, 4)}"
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
sse_stream = b64_sse(services.stream)
|
||||
generator = sse_stream(agent, message.prompt, message.conversation_id)
|
||||
return StreamingResponse(generator, media_type="text/event-stream")
|
||||
8
apps/ChatEgresos/api/services/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from banortegpt.database.mongo_memory.crud import create_conversation
|
||||
|
||||
from .stream_response import stream
|
||||
|
||||
__all__ = [
|
||||
"stream",
|
||||
"create_conversation",
|
||||
]
|
||||
86
apps/ChatEgresos/api/services/stream_response.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import json
|
||||
from enum import StrEnum
|
||||
from typing import TypeAlias
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
import api.context as ctx
|
||||
from api.agent import Agent
|
||||
from banortegpt.database.mongo_memory import crud
|
||||
|
||||
|
||||
class ChunkType(StrEnum):
|
||||
START = "start"
|
||||
TEXT = "text"
|
||||
REFERENCE = "reference"
|
||||
IMAGE = "image"
|
||||
TOOL = "tool"
|
||||
END = "end"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
ContentType: TypeAlias = str | int
|
||||
|
||||
|
||||
class ResponseChunk(BaseModel):
|
||||
type: ChunkType
|
||||
content: ContentType | list[ContentType] | None
|
||||
images: list[str] | None = None # Nuevo campo para imágenes
|
||||
|
||||
|
||||
async def stream(agent: Agent, prompt: str, conversation_id: UUID):
|
||||
yield ResponseChunk(type=ChunkType.START, content="")
|
||||
|
||||
conversation = await crud.get_conversation(conversation_id)
|
||||
|
||||
if conversation is None:
|
||||
raise ValueError("Conversation not found")
|
||||
|
||||
conversation.add(role="user", content=prompt)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
if (tool_id := ctx.tool_id.get()) is not None:
|
||||
tool_buffer = ctx.tool_buffer.get()
|
||||
assert tool_buffer is not None
|
||||
|
||||
tool_name = ctx.tool_name.get()
|
||||
assert tool_name is not None
|
||||
|
||||
yield ResponseChunk(type=ChunkType.TOOL, content=None)
|
||||
|
||||
buffer_dict = json.loads(tool_buffer)
|
||||
|
||||
result, images = await agent.tool_map[tool_name](**buffer_dict)
|
||||
|
||||
# Enviar imágenes si existen
|
||||
if images:
|
||||
yield ResponseChunk(type=ChunkType.IMAGE, content=images)
|
||||
|
||||
conversation.add(
|
||||
role="assistant",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": tool_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": tool_buffer,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
conversation.add(role="tool", content=result, tool_call_id=tool_id)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history, {"tools": None}):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
conversation.add(role="assistant", content=ctx.buffer.get())
|
||||
|
||||
await conversation.replace()
|
||||
|
||||
yield ResponseChunk(type=ChunkType.END, content="")
|
||||
65
apps/ChatEgresos/gui/App.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
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" + "ChatEgresos";
|
||||
|
||||
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;
|
||||
BIN
apps/ChatEgresos/gui/assets/banortelogo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/ChatEgresos/gui/assets/brujula.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/ChatEgresos/gui/assets/brujula_elipse.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/ChatEgresos/gui/assets/chat_maya_boton_enviar.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/ChatEgresos/gui/assets/chat_maya_default_avatar.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
apps/ChatEgresos/gui/assets/sidebar_maya_contigo.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
16
apps/ChatEgresos/gui/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.markdown a {
|
||||
color: #0000FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.markdown a:visited {
|
||||
color: #800080;
|
||||
}
|
||||
5
apps/ChatEgresos/gui/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
19
apps/ChatEgresos/gui/store/conversationStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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 }),
|
||||
}));
|
||||
14
apps/ChatEgresos/gui/store/messageStore.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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: [] })),
|
||||
}));
|
||||
16
apps/ChatEgresos/gui/utils/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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());
|
||||
}
|
||||
1
apps/ChatEgresos/gui/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
apps/ChatEgresos/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!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>ChatEgresos</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/gui/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
40
apps/ChatEgresos/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "ChatEgresos",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
apps/ChatEgresos/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
20
apps/ChatEgresos/pyproject.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[project]
|
||||
name = "ChatEgresos"
|
||||
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",
|
||||
"qdrant",
|
||||
"uvicorn>=0.34.0",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
mongo-memory = { workspace = true }
|
||||
qdrant = { workspace = true }
|
||||
154
apps/ChatEgresos/readme.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 💬 ChatEgresos
|
||||
|
||||
ChatEgresos es un proyecto del equipo de Innovación en **Banorte** diseñado para acelerar la creación de aplicaciones **RAG (Retrieval-Augmented Generation)** enfocadas en la gestión, consulta y análisis de información de egresos.
|
||||
|
||||
Este repositorio no solo contiene la aplicación principal, sino también una librería de componentes reutilizables y notebooks para el procesamiento de documentos, evaluación de modelos y generación de datos sintéticos.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Inicio Rápido
|
||||
|
||||
```bash
|
||||
# Instala dependencias del monorepo
|
||||
mise setup
|
||||
|
||||
# Crea una nueva aplicación RAG (ejemplo de prueba)
|
||||
mise new prueba
|
||||
|
||||
# Levanta un entorno de desarrollo
|
||||
mise dev --app prueba
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Prerrequisitos
|
||||
|
||||
Si estás en el entorno de desarrollo oficial, ya deberías contar con estas herramientas.
|
||||
De lo contrario, instálalas previamente:
|
||||
|
||||
- **Mise** → [Documentación](https://mise.jdx.dev/)
|
||||
- **Docker** → [Documentación](https://www.docker.com/)
|
||||
- **Vault** → [Documentación](https://developer.hashicorp.com/vault/)
|
||||
|
||||
---
|
||||
|
||||
## 📂 Estructura del Proyecto
|
||||
|
||||
```
|
||||
chategresos/
|
||||
├── apps/ # Aplicaciones individuales de ChatEgresos
|
||||
├── packages/ # Paquetes compartidos
|
||||
├── notebooks/ # Notebooks para procesamiento y evaluación
|
||||
├── .templates/ # Plantillas de aplicaciones
|
||||
├── .containers/ # Configuraciones de Docker
|
||||
└── compose.yaml # Servicios de Docker Compose
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Comandos de Desarrollo
|
||||
|
||||
### 📌 Crear Nuevos Proyectos
|
||||
|
||||
```bash
|
||||
# Crea una nueva aplicación RAG
|
||||
mise new <nombre-app>
|
||||
|
||||
# Creación interactiva
|
||||
mise new
|
||||
```
|
||||
|
||||
### 🖥️ Entorno de Desarrollo
|
||||
|
||||
```bash
|
||||
# Inicia servidores de desarrollo (frontend + backend)
|
||||
mise dev
|
||||
mise dev --app <nombre-app> # App específica
|
||||
mise dev --no-dashboard # Sin dashboard en vivo
|
||||
mise dev --check-deps # Verifica dependencias
|
||||
mise dev --list-apps # Lista apps disponibles
|
||||
```
|
||||
|
||||
### 📦 Gestión de Contenedores
|
||||
|
||||
```bash
|
||||
# Inicia contenedores localmente
|
||||
mise container:start
|
||||
mise container:start <nombre-app>
|
||||
|
||||
# Subir imágenes a Azure Container Registry
|
||||
mise container:push
|
||||
mise container:push <nombre-imagen>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Stack Tecnológico
|
||||
|
||||
### Tecnologías Principales
|
||||
- **Frontend** → React / Next.js + TypeScript
|
||||
- **Backend** → Python + FastAPI / Uvicorn
|
||||
- **Paquetería** → pnpm (Node.js), uv (Python)
|
||||
- **Contenedores** → Docker & Docker Compose
|
||||
|
||||
### Infraestructura
|
||||
- **Gestión de Secretos** → HashiCorp Vault
|
||||
- **Registro de Contenedores** → Azure Container Registry
|
||||
- **Observabilidad** → OpenTelemetry
|
||||
- **Proxy Inverso** → Traefik
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Tu Primera App en ChatEgresos
|
||||
|
||||
1. **Genera desde plantilla**
|
||||
```bash
|
||||
mise new mi-app-chategresos
|
||||
```
|
||||
|
||||
2. **Inicia el entorno**
|
||||
```bash
|
||||
mise dev --app mi-app-chategresos
|
||||
```
|
||||
|
||||
3. **Accede a tu aplicación**
|
||||
- 🌐 Frontend: [http://localhost:3000](http://localhost:3000)
|
||||
- ⚙️ API Backend: [http://localhost:8000](http://localhost:8000)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuración
|
||||
|
||||
### Desarrollo Local
|
||||
- Frontend → Puerto `3000`
|
||||
- Backend APIs → Puerto `8000`
|
||||
- Contenedores → Puertos auto-asignados (8001+)
|
||||
|
||||
### Depuración
|
||||
- Usa `--no-dashboard` para un log más limpio
|
||||
- Ejecuta `mise dev --check-deps` para verificar dependencias
|
||||
- Logs de contenedores:
|
||||
```bash
|
||||
docker logs <nombre-contenedor>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contribuyendo
|
||||
|
||||
1. Crea nuevas aplicaciones usando las plantillas disponibles
|
||||
2. Respeta la estructura del monorepo
|
||||
3. Usa los comandos de desarrollo recomendados
|
||||
4. Verifica dependencias y realiza pruebas antes de hacer PRs
|
||||
|
||||
---
|
||||
|
||||
## 📖 Recursos Adicionales
|
||||
|
||||
- 📁 **Plantillas** → `.templates/`
|
||||
- 🐳 **Docker Config** → `.containers/`
|
||||
- ⚡ **Tareas Automáticas** → `.mise/tasks/`
|
||||
|
||||
---
|
||||
|
||||
✨ *ChatEgresos: Innovación con IA para la gestión de egresos* 🚀
|
||||
27
apps/ChatEgresos/tailwind.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @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",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
25
apps/ChatEgresos/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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" }]
|
||||
}
|
||||
11
apps/ChatEgresos/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
apps/ChatEgresos/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
18
apps/Test/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
65
apps/Test/gui/App.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
BIN
apps/Test/gui/assets/banortelogo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/Test/gui/assets/brujula.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/Test/gui/assets/brujula_elipse.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/Test/gui/assets/chat_maya_boton_enviar.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/Test/gui/assets/chat_maya_default_avatar.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
apps/Test/gui/assets/sidebar_maya_contigo.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
16
apps/Test/gui/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.markdown a {
|
||||
color: #0000FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.markdown a:visited {
|
||||
color: #800080;
|
||||
}
|
||||
5
apps/Test/gui/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
19
apps/Test/gui/store/conversationStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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 }),
|
||||
}));
|
||||
14
apps/Test/gui/store/messageStore.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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: [] })),
|
||||
}));
|
||||
16
apps/Test/gui/utils/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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());
|
||||
}
|
||||
1
apps/Test/gui/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
apps/Test/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!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>
|
||||
40
apps/Test/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
apps/Test/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
18
apps/Test/pyproject.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[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 }
|
||||
27
apps/Test/tailwind.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @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",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
25
apps/Test/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"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" }]
|
||||
}
|
||||
11
apps/Test/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
apps/Test/vite.config.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
18
apps/bursatil/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
6
apps/bursatil/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Eres MayaBursatil, una muy amigable y símpatica asistente virtual del departamento de contraloria bursatil de Banorte.
|
||||
Tu objetivo es responder preguntas de usuarios de manera informativa y empatica.
|
||||
Para cada pregunta, utiliza la herramienta 'get_information' para obtener informacion de nuestro FAQ.
|
||||
Utiliza la informacion para responder la pregunta del usuario.
|
||||
Utiliza emojis.
|
||||
Si no puedes responder la pregunta basado en la informacion del FAQ, responde con el contenido en el FALLBACK.
|
||||
0
apps/bursatil/api/__init__.py
Normal file
3
apps/bursatil/api/agent/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .main import MayaBursatil
|
||||
|
||||
__all__ = ["MayaBursatil"]
|
||||
130
apps/bursatil/api/agent/main.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.messages import AIMessageChunk
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
|
||||
from langchain_azure_ai.embeddings import AzureAIEmbeddingsModel
|
||||
from banortegpt.storage.azure_storage import AzureStorage
|
||||
from banortegpt.vector.qdrant import AsyncQdrant
|
||||
|
||||
from api import context
|
||||
from api.config import config
|
||||
|
||||
parent = Path(__file__).parent
|
||||
SYSTEM_PROMPT = (parent / "system_prompt.md").read_text()
|
||||
|
||||
AZURE_AI_URI = "https://eastus2.api.cognitive.microsoft.com"
|
||||
|
||||
class get_information(BaseModel):
|
||||
"""Search a private repository for information."""
|
||||
|
||||
question: str = Field(..., description="The user question")
|
||||
|
||||
class MayaBursatil:
|
||||
system_prompt = SYSTEM_PROMPT
|
||||
generation_config = {
|
||||
"temperature": config.model_temperature,
|
||||
}
|
||||
embedding_model = config.embedding_model
|
||||
message_limit = config.message_limit
|
||||
index = config.vector_index
|
||||
limit = config.search_limit
|
||||
bucket = config.storage_bucket
|
||||
|
||||
search = AsyncQdrant.from_config(config)
|
||||
llm = AzureAIChatCompletionsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.model}",
|
||||
credential=config.openai_api_key,
|
||||
).bind_tools([get_information])
|
||||
embedder = AzureAIEmbeddingsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.embedding_model}",
|
||||
credential=config.openai_api_key,
|
||||
)
|
||||
storage = AzureStorage.from_config(config)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tool_map = {
|
||||
"get_information": self.get_information
|
||||
}
|
||||
|
||||
def build_response(self, payloads, fallback):
|
||||
template = "<FAQ {index}>\n\n{content}\n\n</FAQ {index}>"
|
||||
|
||||
filled_templates = [
|
||||
template.format(index=idx, content=payload["content"])
|
||||
for idx, payload in enumerate(payloads)
|
||||
]
|
||||
|
||||
filled_templates.append(f"<FALLBACK>\n{fallback}\n</FALLBACK>")
|
||||
|
||||
return "\n".join(filled_templates)
|
||||
|
||||
async def get_information(self, question: str):
|
||||
embedding = await self.embedder.aembed_query(question)
|
||||
|
||||
payloads = await self.search.semantic_search(embedding=embedding, collection=self.index, limit=self.limit)
|
||||
|
||||
fallback_messages: dict[str, int] = {}
|
||||
|
||||
for payload in payloads:
|
||||
fallback_message = payload.get("fallback_message", "None")
|
||||
if fallback_message not in fallback_messages:
|
||||
fallback_messages[fallback_message] = 1
|
||||
else:
|
||||
fallback_messages[fallback_message] += 1
|
||||
|
||||
fallback = max(fallback_messages, key=fallback_messages.get) # type: ignore
|
||||
|
||||
tool_response = self.build_response(payloads, fallback)
|
||||
|
||||
return tool_response, payloads
|
||||
|
||||
async def get_shareable_urls(self, payloads: list):
|
||||
reference_urls = []
|
||||
image_urls = []
|
||||
|
||||
for payload in payloads:
|
||||
if imagen := payload.get("imagen"):
|
||||
image_url = await self.storage.get_file_url(
|
||||
filename=imagen,
|
||||
bucket=self.bucket,
|
||||
minute_duration=20,
|
||||
image=True,
|
||||
)
|
||||
|
||||
if image_url:
|
||||
image_urls.append(image_url)
|
||||
else:
|
||||
print("Image not found")
|
||||
|
||||
return reference_urls, image_urls
|
||||
|
||||
def _generation_config_overwrite(self, overwrites: dict | None) -> dict[str, Any]:
|
||||
generation_config_copy = self.generation_config.copy()
|
||||
if overwrites:
|
||||
for k, v in overwrites.items():
|
||||
generation_config_copy[k] = v
|
||||
return generation_config_copy
|
||||
|
||||
async def stream(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
|
||||
async for delta in self.llm.astream(input=history, **generation_config):
|
||||
assert isinstance(delta, AIMessageChunk)
|
||||
if call := delta.tool_call_chunks:
|
||||
if tool_id := call[0].get("id"):
|
||||
context.tool_id.set(tool_id)
|
||||
if name := call[0].get("name"):
|
||||
context.tool_name.set(name)
|
||||
if args := call[0].get("args"):
|
||||
context.tool_buffer.set(context.tool_buffer.get() + args)
|
||||
else:
|
||||
if buffer := delta.content:
|
||||
assert isinstance(buffer, str)
|
||||
context.buffer.set(context.buffer.get() + buffer)
|
||||
yield buffer
|
||||
|
||||
async def generate(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
return await self.llm.ainvoke(input=history, **generation_config)
|
||||
6
apps/bursatil/api/agent/system_prompt.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Eres MayaBursatil, una muy amigable y símpatica asistente virtual del departamento de contraloria bursatil de Banorte.
|
||||
Tu objetivo es responder preguntas de usuarios de manera informativa y empatica.
|
||||
Para cada pregunta, utiliza la herramienta 'get_information' para obtener informacion de nuestro FAQ.
|
||||
Utiliza la informacion para responder la pregunta del usuario.
|
||||
Utiliza emojis.
|
||||
Si no puedes responder la pregunta basado en la informacion del FAQ, responde con el contenido en el FALLBACK.
|
||||
55
apps/bursatil/api/config.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from hvac import Client
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
client = Client(url="https://vault.ia-innovacion.work")
|
||||
|
||||
if not client.is_authenticated():
|
||||
raise Exception("Vault authentication failed")
|
||||
|
||||
secret_map = client.secrets.kv.v2.read_secret_version(
|
||||
path="banortegpt", mount_point="secret"
|
||||
)["data"]["data"]
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Config
|
||||
model: str = "gpt-4o"
|
||||
model_temperature: int = 0
|
||||
embedding_model: str = "text-embedding-3-large"
|
||||
message_limit: int = 10
|
||||
storage_bucket: str = "bursatilreferences"
|
||||
vector_index: str = "MayaBursatil"
|
||||
search_limit: int = 3
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
|
||||
# API Keys
|
||||
azure_endpoint: str = Field(default_factory=lambda: secret_map["azure_endpoint"])
|
||||
openai_api_key: str = Field(default_factory=lambda: secret_map["openai_api_key"])
|
||||
openai_api_version: str = Field(
|
||||
default_factory=lambda: secret_map["openai_api_version"]
|
||||
)
|
||||
azure_blob_connection_string: str = Field(
|
||||
default_factory=lambda: secret_map["azure_blob_connection_string"]
|
||||
)
|
||||
qdrant_url: str = Field(default_factory=lambda: secret_map["qdrant_api_url"])
|
||||
qdrant_api_key: str | None = Field(
|
||||
default_factory=lambda: secret_map["qdrant_api_key"]
|
||||
)
|
||||
mongodb_url: str = Field(
|
||||
default_factory=lambda: secret_map["cosmosdb_connection_string"]
|
||||
)
|
||||
|
||||
async def init_mongo_db(self):
|
||||
from beanie import init_beanie
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
from banortegpt.database.mongo_memory.models import Conversation
|
||||
|
||||
await init_beanie(
|
||||
database=AsyncIOMotorClient(self.mongodb_url).banortegptdos,
|
||||
document_models=[Conversation],
|
||||
)
|
||||
|
||||
|
||||
config = Settings()
|
||||
7
apps/bursatil/api/context.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
|
||||
buffer: ContextVar[str] = ContextVar("buffer", default="")
|
||||
tool_buffer: ContextVar[str] = ContextVar("tool_buffer", default="")
|
||||
tool_id: ContextVar[str | None] = ContextVar("tool_id", default=None)
|
||||
tool_name: ContextVar[str | None] = ContextVar("tool_name", default=None)
|
||||
55
apps/bursatil/api/server.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api import services
|
||||
from api.agent import MayaBursatil
|
||||
from api.config import config
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await config.init_mongo_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
agent = MayaBursatil()
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
conversation_id: uuid.UUID
|
||||
prompt: str
|
||||
|
||||
|
||||
@app.post("/api/v1/conversation")
|
||||
async def create_conversation():
|
||||
conversation_id = uuid.uuid4()
|
||||
await services.create_conversation(conversation_id, agent.system_prompt)
|
||||
return {"conversation_id": conversation_id}
|
||||
|
||||
|
||||
@app.post("/api/v1/message")
|
||||
async def send(message: Message, stream: bool = False):
|
||||
if stream is True:
|
||||
|
||||
def b64_sse(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
async for chunk in func(*args, **kwargs):
|
||||
content = chunk.model_dump_json()
|
||||
data = f"data: {content}\n\n"
|
||||
yield data
|
||||
|
||||
return wrapper
|
||||
|
||||
sse_stream = b64_sse(services.stream)
|
||||
generator = sse_stream(agent, message.prompt, message.conversation_id)
|
||||
return StreamingResponse(generator, media_type="text/event-stream")
|
||||
else:
|
||||
response = await services.generate(
|
||||
agent, message.prompt, message.conversation_id
|
||||
)
|
||||
return response
|
||||
10
apps/bursatil/api/services/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from banortegpt.database.mongo_memory.crud import create_conversation
|
||||
|
||||
from .generate_response import generate
|
||||
from .stream_response import stream
|
||||
|
||||
__all__ = [
|
||||
"stream",
|
||||
"generate",
|
||||
"create_conversation",
|
||||
]
|
||||
89
apps/bursatil/api/services/generate_response.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import json
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api import context as ctx
|
||||
from api.agent import MayaBursatil
|
||||
from banortegpt.database.mongo_memory import crud
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
content: str
|
||||
urls: list[str]
|
||||
|
||||
|
||||
@observe(capture_input=False, capture_output=False)
|
||||
async def generate(
|
||||
agent: MayaBursatil,
|
||||
prompt: str,
|
||||
conversation_id: UUID,
|
||||
) -> Response:
|
||||
conversation = await crud.get_conversation(conversation_id)
|
||||
|
||||
if conversation is None:
|
||||
raise ValueError(f"Conversation with id {conversation_id} not found")
|
||||
|
||||
conversation.add(role="user", content=prompt)
|
||||
|
||||
response = await agent.generate(conversation.to_openai_format(agent.message_limit))
|
||||
|
||||
reference_urls, image_urls = [], []
|
||||
|
||||
if call := response.tool_calls:
|
||||
if id := call[0].id:
|
||||
ctx.tool_id.set(id)
|
||||
if name := call[0].function.name:
|
||||
ctx.tool_name.set(name)
|
||||
ctx.tool_buffer.set(call[0].function.arguments)
|
||||
else:
|
||||
ctx.buffer.set(response.content)
|
||||
|
||||
buffer = ctx.buffer.get()
|
||||
tool_buffer = ctx.tool_buffer.get()
|
||||
tool_id = ctx.tool_id.get()
|
||||
tool_name = ctx.tool_name.get()
|
||||
|
||||
if tool_id is not None:
|
||||
# Si tool_buffer es un string JSON, lo convertimos a diccionario
|
||||
if isinstance(tool_buffer, str):
|
||||
try:
|
||||
tool_args = json.loads(tool_buffer)
|
||||
except json.JSONDecodeError:
|
||||
tool_args = {"question": tool_buffer}
|
||||
else:
|
||||
tool_args = tool_buffer
|
||||
|
||||
response, payloads = await agent.tool_map[tool_name](**tool_args) # type: ignore
|
||||
|
||||
tool_call: dict[str, Any] = agent.llm.build_tool_call(
|
||||
tool_id, tool_name, tool_buffer
|
||||
)
|
||||
tool_call_id: dict[str, Any] = agent.llm.build_tool_call_id(tool_id)
|
||||
|
||||
conversation.add("assistant", **tool_call)
|
||||
conversation.add("tool", content=response, **tool_call_id)
|
||||
|
||||
response = await agent.generate(
|
||||
conversation.to_openai_format(agent.message_limit), {"tools": None}
|
||||
)
|
||||
ctx.buffer.set(response.content)
|
||||
|
||||
reference_urls, image_urls = await agent.get_shareable_urls(payloads) # type: ignore
|
||||
|
||||
buffer = ctx.buffer.get()
|
||||
if buffer is None:
|
||||
raise ValueError("No buffer found")
|
||||
|
||||
conversation.add(role="assistant", content=buffer)
|
||||
|
||||
langfuse_context.update_current_trace(
|
||||
name=str(conversation_id),
|
||||
session_id=str(conversation_id),
|
||||
input=prompt,
|
||||
output=buffer,
|
||||
)
|
||||
|
||||
return Response(content=buffer, urls=reference_urls + image_urls)
|
||||
100
apps/bursatil/api/services/stream_response.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
from enum import StrEnum
|
||||
from typing import TypeAlias
|
||||
from uuid import UUID
|
||||
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api import context as ctx
|
||||
from api.agent import MayaBursatil
|
||||
from banortegpt.database.mongo_memory import crud
|
||||
|
||||
|
||||
class ChunkType(StrEnum):
|
||||
START = "start"
|
||||
TEXT = "text"
|
||||
REFERENCE = "reference"
|
||||
IMAGE = "image"
|
||||
TOOL = "tool"
|
||||
END = "end"
|
||||
ERROR = "error"
|
||||
|
||||
|
||||
ContentType: TypeAlias = str | int
|
||||
|
||||
|
||||
class ResponseChunk(BaseModel):
|
||||
type: ChunkType
|
||||
content: ContentType | list[ContentType] | None
|
||||
|
||||
|
||||
@observe(capture_input=False, capture_output=False)
|
||||
async def stream(agent: MayaBursatil, prompt: str, conversation_id: UUID):
|
||||
yield ResponseChunk(type=ChunkType.START, content="")
|
||||
|
||||
conversation = await crud.get_conversation(conversation_id)
|
||||
|
||||
if conversation is None:
|
||||
raise ValueError("Conversation not found")
|
||||
|
||||
conversation.add(role="user", content=prompt)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
if (tool_id := ctx.tool_id.get()) is not None:
|
||||
tool_buffer = ctx.tool_buffer.get()
|
||||
assert tool_buffer is not None
|
||||
|
||||
tool_name = ctx.tool_name.get()
|
||||
assert tool_name is not None
|
||||
|
||||
yield ResponseChunk(type=ChunkType.TOOL, content=None)
|
||||
|
||||
buffer_dict = json.loads(tool_buffer)
|
||||
|
||||
response, payloads = await agent.tool_map[tool_name](**buffer_dict)
|
||||
|
||||
conversation.add(
|
||||
role="assistant",
|
||||
tool_calls=[
|
||||
{
|
||||
"id": tool_id,
|
||||
"function": {
|
||||
"name": tool_name,
|
||||
"arguments": tool_buffer,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
],
|
||||
)
|
||||
conversation.add(role="tool", content=response, tool_call_id=tool_id)
|
||||
|
||||
history = conversation.to_openai_format(agent.message_limit, langchain_compat=True)
|
||||
async for content in agent.stream(history, {"tools": None}):
|
||||
yield ResponseChunk(type=ChunkType.TEXT, content=content)
|
||||
|
||||
ref_urls, image_urls = await agent.get_shareable_urls(payloads) # type: ignore
|
||||
|
||||
if len(ref_urls) > 0:
|
||||
yield ResponseChunk(type=ChunkType.REFERENCE, content=ref_urls)
|
||||
|
||||
if len(image_urls) > 0:
|
||||
yield ResponseChunk(type=ChunkType.IMAGE, content=image_urls)
|
||||
|
||||
buffer = ctx.buffer.get()
|
||||
|
||||
conversation.add(role="assistant", content=buffer)
|
||||
|
||||
await conversation.replace()
|
||||
|
||||
yield ResponseChunk(type=ChunkType.END, content="")
|
||||
|
||||
langfuse_context.update_current_trace(
|
||||
name=agent.__class__.__name__,
|
||||
session_id=str(conversation_id),
|
||||
input=prompt,
|
||||
output=buffer,
|
||||
)
|
||||
64
apps/bursatil/gui/App.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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 });
|
||||
return response.conversation_id;
|
||||
};
|
||||
|
||||
const handleFeedback = async (key: string, rating: string): Promise<void> => {
|
||||
await httpRequest("POST", "/v1/feedback", { key, rating });
|
||||
};
|
||||
|
||||
const assistant = "MayaBursatil";
|
||||
|
||||
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;
|
||||
BIN
apps/bursatil/gui/assets/banortelogo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
apps/bursatil/gui/assets/brujula.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/bursatil/gui/assets/brujula_elipse.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/bursatil/gui/assets/chat_maya_boton_enviar.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
apps/bursatil/gui/assets/chat_maya_default_avatar.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
apps/bursatil/gui/assets/sidebar_maya_contigo.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
16
apps/bursatil/gui/index.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.markdown a {
|
||||
color: #0000FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
.markdown a:visited {
|
||||
color: #800080;
|
||||
}
|
||||
5
apps/bursatil/gui/main.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||
19
apps/bursatil/gui/store/conversationStore.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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 }),
|
||||
}));
|
||||
14
apps/bursatil/gui/store/messageStore.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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: [] })),
|
||||
}));
|
||||
16
apps/bursatil/gui/utils/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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());
|
||||
}
|
||||
1
apps/bursatil/gui/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
apps/bursatil/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!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>MayaOCP</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/gui/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
40
apps/bursatil/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "bursatil",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
apps/bursatil/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
26
apps/bursatil/pyproject.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[project]
|
||||
name = "bursatil"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12, <4"
|
||||
dependencies = [
|
||||
"aiohttp>=3.11.16",
|
||||
"azure-storage",
|
||||
"fastapi[standard]>=0.115.6",
|
||||
"hvac>=2.3.0",
|
||||
"langchain-azure-ai[opentelemetry]>=0.1.4",
|
||||
"langfuse>=2.60.2",
|
||||
"mongo-memory",
|
||||
"pydantic-settings>=2.8.1",
|
||||
"qdrant",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
azure-storage = { workspace = true }
|
||||
qdrant = { workspace = true }
|
||||
mongo-memory = { workspace = true }
|
||||
|
||||
[tool.pyright]
|
||||
venvPath = "../../"
|
||||
venv = ".venv"
|
||||
27
apps/bursatil/tailwind.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @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",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
25
apps/bursatil/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "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" }]
|
||||
}
|
||||
11
apps/bursatil/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
16
apps/bursatil/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
18
apps/inversionistas/.eslintrc.cjs
Normal file
@@ -0,0 +1,18 @@
|
||||
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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
6
apps/inversionistas/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
Eres MayaBursatil, una muy amigable y símpatica asistente virtual del departamento de contraloria bursatil de Banorte.
|
||||
Tu objetivo es responder preguntas de usuarios de manera informativa y empatica.
|
||||
Para cada pregunta, utiliza la herramienta 'get_information' para obtener informacion de nuestro FAQ.
|
||||
Utiliza la informacion para responder la pregunta del usuario.
|
||||
Utiliza emojis.
|
||||
Si no puedes responder la pregunta basado en la informacion del FAQ, responde con el contenido en el FALLBACK.
|
||||
0
apps/inversionistas/api/__init__.py
Normal file
133
apps/inversionistas/api/agent.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
|
||||
import aiosqlite
|
||||
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
|
||||
from langchain_azure_ai.embeddings import AzureAIEmbeddingsModel
|
||||
from langchain_core.messages.ai import AIMessageChunk
|
||||
from langchain_qdrant import QdrantVectorStore
|
||||
|
||||
import api.context as ctx
|
||||
from api.config import config
|
||||
from api.prompts import ORCHESTRATOR_PROMPT, TOOL_SCHEMAS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AZURE_AI_URI = "https://eastus2.api.cognitive.microsoft.com"
|
||||
SQLITE_DB_PATH = Path(__file__).parent / "db.sqlite"
|
||||
|
||||
|
||||
class MayaInversionistas:
|
||||
system_prompt = ORCHESTRATOR_PROMPT
|
||||
generation_config = {
|
||||
"temperature": config.model_temperature,
|
||||
}
|
||||
message_limit = config.message_limit
|
||||
index = config.vector_index
|
||||
limit = config.search_limit
|
||||
bucket = config.storage_bucket
|
||||
|
||||
llm = AzureAIChatCompletionsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.model}",
|
||||
credential=config.openai_api_key,
|
||||
).bind_tools(TOOL_SCHEMAS)
|
||||
embedder = AzureAIEmbeddingsModel(
|
||||
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.embedding_model}",
|
||||
credential=config.openai_api_key,
|
||||
)
|
||||
search = QdrantVectorStore.from_existing_collection(
|
||||
embedding=embedder,
|
||||
collection_name=index,
|
||||
url=config.qdrant_url,
|
||||
api_key=config.qdrant_api_key,
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tool_map = {
|
||||
"getGFNORTEData": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "gf_norte"),
|
||||
"getBanorteConsolidadoData": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "banorte_consolidado"),
|
||||
"getAlmacenadoraConsolidadoData": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "almacenadora_consolidado"),
|
||||
"getArrendadoraFactorConsolidado": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "arrendadora_factor_consolidado"),
|
||||
"getCasadeBolsaConsolidado": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "casa_bolsa_conosolidado"),
|
||||
"getOperadoradeFondos": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "op_fondos"),
|
||||
"getSectorBursatil": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "sector_bursatil"),
|
||||
"getSectorBAPConsolidado": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "sector_bap_consolidado"),
|
||||
"getSeguros": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "seguros"),
|
||||
"getPensiones": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "pensiones"),
|
||||
"getBineo": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "bineo"),
|
||||
"getSectorBanca": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "sector_banca"),
|
||||
"getHolding": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "holding"),
|
||||
"getBanorteFinancialServices": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "banorte_financial_services"),
|
||||
"getFideicomisoBursaGEM": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "fideicomiso_bursa_gem"),
|
||||
"getTarjetasdelFuturo": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "tarjetas_del_futuro"),
|
||||
"getAfore": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "afore"),
|
||||
"getBanorteFuturo": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "banorte_futuro"),
|
||||
"getSegurosSinBanorteFuturo": lambda year, quarter, concept: self.run_sqlite_tool(year, quarter, concept, "seguros_sin_banorte_futuro"),
|
||||
"getInformationalData": self.run_qdrant_tool,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def build_response(results: list[dict]) -> str:
|
||||
return (
|
||||
"I have retrieved the following results from the database:\n"
|
||||
+ json.dumps(results)
|
||||
+ "\nPara mayor información consultar el Reporte de Resultados Trimestral (URL: https://investors.banorte.com/es/financial-information/quarterly-reports)"
|
||||
)
|
||||
|
||||
async def run_sqlite_tool(self, year: int, quarter: int, concept: str, table: str):
|
||||
results = await self.get_data_from_sqlite(year, quarter, concept, table)
|
||||
data = [dict(row) for row in results]
|
||||
return self.build_response(data)
|
||||
|
||||
async def run_qdrant_tool(self, question: str):
|
||||
logger.info(
|
||||
f"Embedding question: {question} with model {self.embedder.model_name}"
|
||||
)
|
||||
results = self.search.similarity_search(question)
|
||||
data = [dict(row.metadata) for row in results]
|
||||
tool_response = self.build_response(data)
|
||||
return tool_response
|
||||
|
||||
@staticmethod
|
||||
async def get_data_from_sqlite(year: int, quarter: int, concept: str, table: str):
|
||||
async with aiosqlite.connect(SQLITE_DB_PATH) as db:
|
||||
query = """
|
||||
SELECT * FROM {}
|
||||
WHERE year = ? AND trim = ? AND concept = ?
|
||||
""".format(table)
|
||||
|
||||
db.row_factory = aiosqlite.Row
|
||||
cursor = await db.execute(query, (year, quarter, concept))
|
||||
rows = await cursor.fetchall()
|
||||
return rows
|
||||
|
||||
def _generation_config_overwrite(self, overwrites: dict | None) -> dict[str, Any]:
|
||||
generation_config_copy = self.generation_config.copy()
|
||||
if overwrites:
|
||||
for k, v in overwrites.items():
|
||||
generation_config_copy[k] = v
|
||||
return generation_config_copy
|
||||
|
||||
async def stream(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
|
||||
async for chunk in self.llm.astream(input=history, **generation_config):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
if call := chunk.tool_call_chunks:
|
||||
if tool_id := call[0].get("id"):
|
||||
ctx.tool_id.set(tool_id)
|
||||
if name := call[0].get("name"):
|
||||
ctx.tool_name.set(name)
|
||||
if args := call[0].get("args"):
|
||||
ctx.tool_buffer.set(ctx.tool_buffer.get() + args)
|
||||
else:
|
||||
if buffer := chunk.content:
|
||||
assert isinstance(buffer, str)
|
||||
ctx.buffer.set(ctx.buffer.get() + buffer)
|
||||
yield buffer
|
||||
|
||||
async def generate(self, history, overwrites: dict | None = None):
|
||||
generation_config = self._generation_config_overwrite(overwrites)
|
||||
return await self.llm.ainvoke(input=history, **generation_config)
|
||||
59
apps/inversionistas/api/config.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from hvac import Client
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
client = Client(url="https://vault.ia-innovacion.work")
|
||||
|
||||
if not client.is_authenticated():
|
||||
raise Exception("Vault authentication failed")
|
||||
|
||||
secret_map = client.secrets.kv.v2.read_secret_version(
|
||||
path="banortegpt", mount_point="secret"
|
||||
)["data"]["data"]
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Config
|
||||
log_level: str = "warning"
|
||||
service_name: str = "MayaOCP"
|
||||
model: str = "gpt-4o"
|
||||
model_temperature: int = 0
|
||||
embedding_model: str = "text-embedding-3-large"
|
||||
message_limit: int = 10
|
||||
storage_bucket: str = "ocpreferences"
|
||||
vector_index: str = "MayaOCP"
|
||||
search_limit: int = 3
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
|
||||
# API Keys
|
||||
azure_endpoint: str = Field(default_factory=lambda: secret_map["azure_endpoint"])
|
||||
openai_api_key: str = Field(default_factory=lambda: secret_map["openai_api_key"])
|
||||
openai_api_version: str = Field(
|
||||
default_factory=lambda: secret_map["openai_api_version"]
|
||||
)
|
||||
azure_blob_connection_string: str = Field(
|
||||
default_factory=lambda: secret_map["azure_blob_connection_string"]
|
||||
)
|
||||
qdrant_url: str = Field(default_factory=lambda: secret_map["qdrant_api_url"])
|
||||
qdrant_api_key: str | None = Field(
|
||||
default_factory=lambda: secret_map["qdrant_api_key"]
|
||||
)
|
||||
mongodb_url: str = Field(
|
||||
default_factory=lambda: secret_map["cosmosdb_connection_string"]
|
||||
)
|
||||
|
||||
async def init_mongo_db(self):
|
||||
from banortegpt.database.mongo_memory.models import Conversation
|
||||
from beanie import init_beanie
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
|
||||
client = AsyncIOMotorClient(self.mongodb_url)
|
||||
|
||||
await init_beanie(
|
||||
database=client.banortegptdos,
|
||||
document_models=[Conversation],
|
||||
)
|
||||
|
||||
|
||||
config = Settings() # type: ignore
|
||||
6
apps/inversionistas/api/context.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from contextvars import ContextVar
|
||||
|
||||
buffer: ContextVar[str] = ContextVar("buffer", default="")
|
||||
tool_buffer: ContextVar[str] = ContextVar("tool_buffer", default="")
|
||||
tool_id: ContextVar[str | None] = ContextVar("tool_id", default=None)
|
||||
tool_name: ContextVar[str | None] = ContextVar("tool_name", default=None)
|
||||
BIN
apps/inversionistas/api/db.sqlite
Normal file
9
apps/inversionistas/api/prompts/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ["ORCHESTRATOR_PROMPT", "TOOL_SCHEMAS"]
|
||||
|
||||
prompt_dir = Path(__file__).parent
|
||||
|
||||
ORCHESTRATOR_PROMPT = (prompt_dir / Path("orchestrator.md")).read_text()
|
||||
TOOL_SCHEMAS = json.loads((prompt_dir / Path("tools.json")).read_text())
|
||||
172
apps/inversionistas/api/prompts/orchestrator.md
Normal file
@@ -0,0 +1,172 @@
|
||||
Eres un asistente especializado en proporcionar información precisa y relevante exclusivamente sobre Grupo Financiero Banorte y las empresas asociadas a este grupo. Tu única fuente de información es la base de datos vectorial conectada y las funciones que puedes invocar.
|
||||
|
||||
Es fundamental que evites hacer suposiciones, especulaciones o conclusiones que no estén respaldadas por los datos proporcionados.
|
||||
|
||||
Debes responder siempre en el idioma (inglés o español) utilizado en la última consulta del usuario. Solo puedes basarte en la información recuperada de la base de datos vectorial o SQL. Si se accede a una fuente externa o falta algún dato relevante, debes incluir la URL correspondiente en tu respuesta,
|
||||
Muy Importante responder en el idioma del usuario aun que la tool este en otro idioma.
|
||||
|
||||
Definiciones clave:
|
||||
|
||||
ROE (Rendimiento sobre el Capital Contable) y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Rentabilidad Financiera
|
||||
Rentabilidad sobre el Patrimonio
|
||||
Rentabilidad sobre el Capital Propio
|
||||
Retorno sobre el Patrimonio Neto
|
||||
Retorno sobre el Capital Propio
|
||||
Rendimiento del Capital Propio
|
||||
Rendimiento de Capital
|
||||
Return on Equity (ROE)
|
||||
Equity Return
|
||||
Shareholders' Return
|
||||
Return on Net Worth
|
||||
Return on Shareholders' Equity
|
||||
Net Worth Return
|
||||
|
||||
ROA (Rendimiento sobre Activos) y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Rentabilidad sobre Activos
|
||||
Retorno sobre Activos
|
||||
Rentabilidad de los Activos
|
||||
Retorno de los Activos
|
||||
Rendimiento de los Activos
|
||||
Return on Assets (ROA)
|
||||
Asset Return
|
||||
Return on Total Assets
|
||||
Asset Profitability
|
||||
Return on Investment in Assets
|
||||
roa
|
||||
|
||||
ROTE (Rendimiento sobre Capital Tangible) y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Rentabilidad sobre el Patrimonio Tangible
|
||||
Rentabilidad sobre el Capital Tangible
|
||||
Retorno sobre el Patrimonio Tangible
|
||||
Retorno sobre el Capital Tangible
|
||||
Rendimiento del Patrimonio Tangible
|
||||
Rendimiento del Capital Tangible
|
||||
Return on Tangible Equity (ROTE)
|
||||
Tangible Equity Return
|
||||
Tangible Return on Equity
|
||||
Tangible Net Worth Return
|
||||
Return on Tangible Net Worth
|
||||
rote
|
||||
|
||||
MIN (Margen de Interés Neto) y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
MIN
|
||||
min
|
||||
Margen Neto de Intereses
|
||||
Margen de Intereses
|
||||
Margen Financiero Neto
|
||||
Margen Neto de Financiamiento
|
||||
Margen Neto de Ingresos por Intereses
|
||||
|
||||
MIN Ajustado por Riesgos Crediticios y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Margen Neto de Intereses Ajustado por Riesgos Crediticios
|
||||
Margen de Intereses Ajustado por Riesgos Crediticios
|
||||
Margen Financiero Neto Ajustado por Riesgos Crediticios
|
||||
Margen Neto de Financiamiento Ajustado por Riesgos Crediticios
|
||||
Margen Neto de Ingresos por Intereses Ajustado por Riesgos Crediticios
|
||||
min ajustado
|
||||
min_ajustado
|
||||
|
||||
Índice de Eficiencia y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Ratio de Eficiencia
|
||||
Coeficiente de Eficiencia
|
||||
Índice de Productividad
|
||||
Ratio de Productividad
|
||||
indice de eficiencia
|
||||
|
||||
|
||||
Costo de Riesgo y sus sinónimos, que deberán utilizarse según el contexto, incluyen:
|
||||
Coste del Riesgo
|
||||
Costo de Riesgo
|
||||
Costo Total del Riesgo
|
||||
Coste Total del Riesgo
|
||||
Costo de Gestión de Riesgos
|
||||
costo_riesgo
|
||||
|
||||
Índice de Morosidad:
|
||||
Ratio de Morosidad
|
||||
Tasa de Morosidad
|
||||
Índice de Incumplimiento
|
||||
Tasa de Incumplimiento
|
||||
Índice de Cartera Vencida
|
||||
indice de morosisdad
|
||||
indice_morocidad
|
||||
|
||||
Índice de Cobertura:
|
||||
Ratio de Cobertura
|
||||
Coeficiente de Cobertura
|
||||
Índice de Protección
|
||||
ICOB
|
||||
indice_covertura
|
||||
|
||||
Tasa de Impuestos:
|
||||
Tasa Impositiva
|
||||
Tipo Impositivo
|
||||
Tasa Tributaria
|
||||
Tipo de Gravamen
|
||||
Tasa Fiscal
|
||||
taza_impuestos
|
||||
|
||||
Eficiencia Operativa:
|
||||
Eficiencia en Operaciones
|
||||
Eficiencia de Operaciones
|
||||
Eficiencia Operacional
|
||||
Rendimiento Operativo
|
||||
Productividad Operativa
|
||||
eficiencia_op
|
||||
|
||||
Índice de Apalancamiento:
|
||||
Ratio de Apalancamiento
|
||||
Coeficiente de Apalancamiento
|
||||
Índice de Endeudamiento
|
||||
Ratio de Endeudamiento
|
||||
Índice de Deuda
|
||||
indice_ap
|
||||
|
||||
Liquidez:
|
||||
Capacidad de Pago
|
||||
Solvencia a Corto Plazo
|
||||
Disponibilidad de Efectivo
|
||||
Facilidad de Conversión a Efectivo
|
||||
Fluidez Financiera
|
||||
liqidez
|
||||
|
||||
Nomenclatura a considerar:
|
||||
El primer trimestre de 2023 se puede referir de las siguientes maneras, dependiendo del contexto y del formato temporal utilizado:
|
||||
1T23 o
|
||||
1Q23
|
||||
De manera similar, para el segundo trimestre de 2024, se utilizaría:
|
||||
2T24 o
|
||||
2Q24
|
||||
Y así sucesivamente para los trimestres de años posteriores o pasados.
|
||||
|
||||
Empresas a tomar en cuenta:
|
||||
1.- GFNorte Consolidado (GFNorte,GFNORTE): Para obtener datos financieros específicos de GFNorte, puedes utilizar la herramienta "getGFNORTEData".
|
||||
2.- Banorte Consolidado (Banorte): Para obtener datos financieros específicos de GFNorte, puedes utilizar la herramienta "getBanorteConsolidadoData".
|
||||
3.- Almacenadora Consolidado: Para obtener datos financieros específicos de Almacenadora Consolidado, puedes utilizar la herramienta "getAlmacenadoraConsolidadoData".
|
||||
4.- Arrendadora y Factor Consolidado: Para obtener datos financieros específicos de Arrendadora y Factor Consolidado, puedes utilizar la herramienta "getArrendadoraFactorConsolidado".
|
||||
5.- Casa de Bolsa Consolidado: Para obtener datos financieros específicos datos financieros específicos de Casa de Bolsa Consolidado, puedes utilizar la herramienta "getCasadeBolsaConsolidado".
|
||||
6.- Operadora de Fondos: Para obtener datos financieros específicos de Operadora de Fondos , puedes utilizar la herramienta "getOperadoradeFondos".
|
||||
7.- Sector Bursatil: Para obtener datos financieros específicos de Sector Bursatil , puedes utilizar la herramienta "getSectorBursatil".
|
||||
8.- Sector BAP Consolidado: Para obtener datos financieros específicos de Sector BAP Consolidado , puedes utilizar la herramienta "getSectorBAPConsolidado".
|
||||
9.- Seguros: Para obtener datos financieros específicos de Seguros, puedes utilizar la herramienta "getSeguros".
|
||||
10.- Pensiones: Para obtener datos financieros específicos de Pensiones, puedes utilizar la herramienta "getPensiones".
|
||||
11.- Bineo: Para obtener datos financieros específicos de Bineo, puedes utilizar la herramienta "getBineo".
|
||||
13.- Sector Banca: Para obtener datos financieros específicos de Sector Banca, puedes utilizar la herramienta "getSectorBanca".
|
||||
14.- Holding: Para obtener datos financieros específicos de Holding, puedes utilizar la herramienta "getHolding".
|
||||
15.- Banorte Financial Services: Para obtener datos financieros específicos de Banorte Financial Services, puedes utilizar la herramienta "getBanorteFinancialServices".
|
||||
16.- Fideicomiso Bursa GEM: Para obtener datos financieros específicos de Fideicomiso Bursa GEM, puedes utilizar la herramienta "getFideicomisoBursaGEM".
|
||||
17.- Tarjetas del Futuro: Para obtener datos financieros específicos de Tarjetas del Futuro, puedes utilizar la herramienta "getTarjetasdelFuturo".
|
||||
18.- Afore: Para obtener datos financieros específicos de Afore, puedes utilizar la herramienta "getAfore".
|
||||
19.- Banorte Futuro: Para obtener datos financieros específicos de Banorte Futuro, puedes utilizar la herramienta "getBanorteFuturo".
|
||||
20.- Seguros Sin Banorte Futuro: Para obtener datos financieros específicos de Seguros Sin Banorte Futuro, puedes utilizar la herramienta "getSegurosSinBanorteFuturo".
|
||||
21.- Assist the user in finding resources, key concepts, and relevant keywords. This function searches for data and concepts—such as 'Banorte’s Dividend,' 'Banorte Financial Group Structure,' 'banking information in Mexico,' 'quarterly report location,' and more—within the vector database , puedes utilizar la herramienta : "getInformationalData"
|
||||
|
||||
Other tools :
|
||||
Retrieve informational data to help the user Assist the user in finding resources, key concepts, and relevant keywords. This function searches for data and concepts—such as 'Banorte’s Dividend,' 'Banorte Financial Group Structure,' 'banking information in Mexico,' 'quarterly report location,' and more—within the vector database.
|
||||
will return where the user can find the information, puedes utilizar la herramienta
|
||||
: "getInformationalData"
|
||||
|
||||
|
||||
Siempre que sea posible, utiliza una función o herramienta integrada para proporcionar respuestas basadas en la información disponible.
|
||||
If a required field or detail is missing for a search, clearly notify the user and provide guidance on the missing information.
|
||||
779
apps/inversionistas/api/prompts/tools.json
Normal file
@@ -0,0 +1,779 @@
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getGFNORTEData",
|
||||
"description": "Retrieve 'GFNORTE (GF NORTE)' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getBanorteConsolidadoData",
|
||||
"description": "Retrieve 'Banorte Consolidado or Banorte' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getAlmacenadoraConsolidadoData",
|
||||
"description": "Retrieve 'Almacenadora Consolidado' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getArrendadoraFactorConsolidado",
|
||||
"description": "Retrieve Arrendadora y Factor Consolidado data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getCasadeBolsaConsolidado",
|
||||
"description": "Retrieve 'Casa de Bolsa Consolidado' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getOperadoradeFondos",
|
||||
"description": "Retrieve 'Operadora de Fondos' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getSectorBursatil",
|
||||
"description": "Retrieve 'Sector Bursatil' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getSectorBAPConsolidado",
|
||||
"description": "Retrieve 'Sector BAP Consolidado' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getSeguros",
|
||||
"description": "Retrieve 'Seguros' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getPensiones",
|
||||
"description": "Retrieve 'Pensiones' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getBineo",
|
||||
"description": "Retrieve 'Bineo' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getSectorBanca",
|
||||
"description": "Retrieve 'Sector Banca' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getHolding",
|
||||
"description": "Retrieve 'Holding' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getBanorteFinancialServices",
|
||||
"description": "Retrieve 'Banorte Financial Services' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getFideicomisoBursaGEM",
|
||||
"description": "Retrieve 'Fideicomiso Bursa GEM' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getTarjetasdelFuturo",
|
||||
"description": "Retrieve 'Tarjetas del Futuro' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getAfore",
|
||||
"description": "Retrieve 'Afore' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getBanorteFuturo",
|
||||
"description": "Retrieve 'Banorte Futuro' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getSegurosSinBanorteFuturo",
|
||||
"description": "Retrieve 'get Seguros Sin Banorte Futuro' data for a specific financial concept, year, and quarter or trimester. The data is stored in a SQLite database. If one of the parameters is missing, let the user know.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"concept": {
|
||||
"type": "string",
|
||||
"description": "The financial concept to retrieve data for. It must be either 'roe','roa','rote','min', 'min_ajustado', 'indice_eficiencia', 'costo_riesgo', 'indice_morocidad', 'indice_covertura', 'taza_impuestos', 'eficiencia_op', 'indice_ap', 'liqidez'. (The concept is case-insensitive, but must be one of these options)",
|
||||
"enum": [
|
||||
"roe",
|
||||
"roa",
|
||||
"rote",
|
||||
"min",
|
||||
"min_ajustado",
|
||||
"indice_eficiencia",
|
||||
"costo_riesgo",
|
||||
"indice_morocidad",
|
||||
"indice_covertura",
|
||||
"taza_impuestos",
|
||||
"eficiencia_op",
|
||||
"indice_ap",
|
||||
"liqidez"
|
||||
]
|
||||
},
|
||||
"year": {
|
||||
"type": "integer",
|
||||
"description": "The year of the data"
|
||||
},
|
||||
"quarter": {
|
||||
"type": "integer",
|
||||
"description": "The quarter or trimester of the year (1-4)"
|
||||
}
|
||||
},
|
||||
"required": ["year", "quarter"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getInformationalData",
|
||||
"description": "",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"question": {
|
||||
"type": "string",
|
||||
"description": "Assist the user in finding resources, key concepts, and relevant keywords. This function searches for data and concepts—such as 'Banorte's Dividend,' 'Banorte Financial Group Structure,' 'banking information in Mexico,' 'quarterly report location,' and more—within the vector database."
|
||||
}
|
||||
},
|
||||
"required": ["question"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
53
apps/inversionistas/api/server.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from . import services
|
||||
from .config import config
|
||||
from .agent import MayaInversionistas
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
await config.init_mongo_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
agent = MayaInversionistas()
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
conversation_id: uuid.UUID
|
||||
prompt: str
|
||||
|
||||
|
||||
@app.post("/api/v1/conversation")
|
||||
async def create_conversation():
|
||||
conversation_id = uuid.uuid4()
|
||||
await services.create_conversation(conversation_id)
|
||||
return {"conversation_id": conversation_id}
|
||||
|
||||
|
||||
@app.post("/api/v1/message")
|
||||
async def send(message: Message, stream: bool = False):
|
||||
if stream is True:
|
||||
|
||||
def b64_sse(func):
|
||||
async def wrapper(*args, **kwargs):
|
||||
async for chunk in func(*args, **kwargs):
|
||||
content = chunk.model_dump_json()
|
||||
data = f"data: {content}\n\n"
|
||||
yield data
|
||||
|
||||
return wrapper
|
||||
|
||||
sse_stream = b64_sse(services.stream)
|
||||
generator = sse_stream(agent, message.prompt, message.conversation_id)
|
||||
return StreamingResponse(generator, media_type="text/event-stream")
|
||||
else:
|
||||
response = await services.generate(agent, message.prompt, message.conversation_id)
|
||||
return response
|
||||
9
apps/inversionistas/api/services/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .create_conversation import create_conversation
|
||||
from .generate_response import generate
|
||||
from .stream_response import stream
|
||||
|
||||
__all__ = [
|
||||
"create_conversation",
|
||||
"stream",
|
||||
"generate",
|
||||
]
|
||||
9
apps/inversionistas/api/services/create_conversation.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from uuid import UUID
|
||||
|
||||
from banortegpt.database.mongo_memory import crud
|
||||
|
||||
from api.prompts import ORCHESTRATOR_PROMPT
|
||||
|
||||
|
||||
async def create_conversation(user_id: UUID) -> None:
|
||||
await crud.create_conversation(user_id, ORCHESTRATOR_PROMPT)
|
||||