ic
31
.containers/marimo/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
|
||||||
|
|
||||||
|
ARG PACKAGE
|
||||||
|
|
||||||
|
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --no-dev --no-install-workspace --package ${PACKAGE}
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY apps ./apps
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-dev --no-editable --package ${PACKAGE}
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS runtime
|
||||||
|
|
||||||
|
ARG PACKAGE
|
||||||
|
|
||||||
|
COPY --from=builder --chown=app:app /app/.venv /venv
|
||||||
|
|
||||||
|
ENV PATH="/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY notebooks/${PACKAGE} /notebook
|
||||||
|
|
||||||
|
CMD ["python3", "-m", "marimo", "run", "notebook/main.py", "--host", "0.0.0.0", "--port", "80"]
|
||||||
51
.containers/unit/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
FROM node:22-slim AS frontend-builder
|
||||||
|
|
||||||
|
ARG PACKAGE
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY apps ./apps
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
RUN pnpm --filter ${PACKAGE} build
|
||||||
|
|
||||||
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS backend-builder
|
||||||
|
|
||||||
|
ARG PACKAGE
|
||||||
|
|
||||||
|
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --no-dev --no-install-workspace --package ${PACKAGE}
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY apps ./apps
|
||||||
|
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync --frozen --no-dev --no-editable --package ${PACKAGE}
|
||||||
|
|
||||||
|
FROM unit:python3.12-slim AS runtime
|
||||||
|
|
||||||
|
ARG PACKAGE
|
||||||
|
|
||||||
|
COPY --from=backend-builder --chown=app:app /app/.venv /venv
|
||||||
|
|
||||||
|
ENV PATH="/venv/bin:$PATH"
|
||||||
|
|
||||||
|
COPY --from=backend-builder --chown=app:app /app/apps/${PACKAGE}/api /api
|
||||||
|
|
||||||
|
COPY --from=frontend-builder --chown=app:app /app/apps/${PACKAGE}/dist /index
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
COPY .containers/unit/unit.json /docker-entrypoint.d/config.json
|
||||||
31
.containers/unit/unit.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"listeners": {
|
||||||
|
"*:80": {
|
||||||
|
"pass": "routes"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"fastapi": {
|
||||||
|
"type": "python3",
|
||||||
|
"path": "/",
|
||||||
|
"module": "api.server",
|
||||||
|
"callable": "app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"match": {
|
||||||
|
"uri": "/api/*"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"pass": "applications/fastapi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"share": "/index$uri",
|
||||||
|
"types": ["image/*", "font/*", "text/*", "application/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
200
.gitignore
vendored
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
node_modules
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env*
|
||||||
20
.mise/config.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[tools]
|
||||||
|
node = "22"
|
||||||
|
pnpm = "latest"
|
||||||
|
python = "3.12"
|
||||||
|
uv = "latest"
|
||||||
|
|
||||||
|
[tasks."container:build"]
|
||||||
|
description = "Build the container for a project"
|
||||||
|
run = "docker buildx bake --no-cache"
|
||||||
|
|
||||||
|
[tasks."secrets:list"]
|
||||||
|
description = "List secrets from Vault"
|
||||||
|
run = """vault kv get -mount="secret" -address=https://vault.ia-innovacion.work -format=json "banortegpt"| jq '.["data"]["data"]'"""
|
||||||
|
silent = true
|
||||||
|
|
||||||
|
[tasks.setup]
|
||||||
|
run = """
|
||||||
|
pnpm install && pnpm build
|
||||||
|
uv sync --all-packages
|
||||||
|
"""
|
||||||
290
.mise/tasks/container/push.py
Executable file
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
#MISE silent=true
|
||||||
|
#MISE description="Push the container to Azure"
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["rich"]
|
||||||
|
# ///
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||||
|
from rich.prompt import Prompt, Confirm
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Azure Container Registry configuration
|
||||||
|
AZURE_REGISTRY = "iaservicecontainers.azurecr.io"
|
||||||
|
IMAGE_PREFIX = "mayacontigo/"
|
||||||
|
|
||||||
|
|
||||||
|
def run_docker_command(command: list[str], description: str) -> bool:
|
||||||
|
"""Run a docker command with rich progress indication."""
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
console=console,
|
||||||
|
transient=True,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task(description, total=None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
progress.update(task, description=f"✅ {description}")
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
progress.update(task, description=f"❌ {description}")
|
||||||
|
console.print(f"[red]Error running command: {' '.join(command)}[/red]")
|
||||||
|
console.print(f"[red]Error output: {e.stderr}[/red]")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
progress.update(task, description=f"❌ {description}")
|
||||||
|
console.print("[red]Docker command not found. Please ensure Docker is installed and in your PATH.[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_docker_image_exists(image_name: str) -> bool:
|
||||||
|
"""Check if a Docker image exists locally."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "images", "-q", image_name],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_images() -> list[str]:
|
||||||
|
"""Get list of available Docker images filtered by prefix."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
images = [line.strip() for line in result.stdout.split('\n') if line.strip()]
|
||||||
|
# Filter images to only include those with the specified prefix
|
||||||
|
filtered_images = [img for img in images if not img.startswith('<none>') and img.startswith(IMAGE_PREFIX)]
|
||||||
|
return filtered_images
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def display_available_images(images: list[str], show_numbers: bool = False) -> None:
|
||||||
|
"""Display available Docker images in a nice table."""
|
||||||
|
if not images:
|
||||||
|
console.print(f"[yellow]No Docker images found locally with prefix '{IMAGE_PREFIX}'.[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(title=f"Available Docker Images (prefix: {IMAGE_PREFIX})")
|
||||||
|
|
||||||
|
if show_numbers:
|
||||||
|
table.add_column("No.", style="bold yellow", width=4)
|
||||||
|
|
||||||
|
table.add_column("Image", style="cyan")
|
||||||
|
table.add_column("Registry Status", style="magenta")
|
||||||
|
|
||||||
|
for i, image in enumerate(images[:10], 1): # Show first 10 images
|
||||||
|
registry_status = "🏷️ Ready to tag" if not image.startswith(AZURE_REGISTRY) else "🚀 Already tagged"
|
||||||
|
|
||||||
|
if show_numbers:
|
||||||
|
table.add_row(str(i), image, registry_status)
|
||||||
|
else:
|
||||||
|
table.add_row(image, registry_status)
|
||||||
|
|
||||||
|
if len(images) > 10:
|
||||||
|
remaining_text = f"[dim]and {len(images) - 10} more[/dim]"
|
||||||
|
if show_numbers:
|
||||||
|
table.add_row("...", "...", remaining_text)
|
||||||
|
else:
|
||||||
|
table.add_row("...", remaining_text)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_mode() -> Optional[str]:
|
||||||
|
"""Run in interactive mode to select an image."""
|
||||||
|
console.print(Panel.fit(
|
||||||
|
"[bold blue]🐳 Docker Image Push to Azure[/bold blue]\n"
|
||||||
|
"[dim]Interactive mode - Let's select an image to push[/dim]",
|
||||||
|
border_style="blue"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Get available images
|
||||||
|
console.print("\n[bold]Fetching available Docker images...[/bold]")
|
||||||
|
images = get_available_images()
|
||||||
|
|
||||||
|
if not images:
|
||||||
|
console.print(f"[red]No Docker images found with prefix '{IMAGE_PREFIX}'. Please build an image first.[/red]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
display_available_images(images, show_numbers=True)
|
||||||
|
|
||||||
|
# Create choices with numbers
|
||||||
|
choices = [str(i) for i in range(1, len(images) + 1)]
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice = Prompt.ask(
|
||||||
|
"Select an image to push",
|
||||||
|
choices=choices,
|
||||||
|
default="1"
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_image = images[int(choice) - 1]
|
||||||
|
console.print(f"[green]✅ Selected image: {selected_image}[/green]")
|
||||||
|
return selected_image
|
||||||
|
|
||||||
|
except (ValueError, IndexError, KeyboardInterrupt):
|
||||||
|
console.print("[yellow]Selection cancelled.[/yellow]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def push_image(image_name: str, force: bool = False) -> bool:
|
||||||
|
"""Push a Docker image to Azure Container Registry."""
|
||||||
|
# Validate image name
|
||||||
|
if not image_name:
|
||||||
|
console.print("[red]Error: Image name cannot be empty[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if image has the required prefix
|
||||||
|
if not image_name.startswith(IMAGE_PREFIX):
|
||||||
|
console.print(f"[red]Error: Image '{image_name}' does not have the required prefix '{IMAGE_PREFIX}'[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if image exists locally
|
||||||
|
if not check_docker_image_exists(image_name):
|
||||||
|
console.print(f"[red]Error: Image '{image_name}' not found locally[/red]")
|
||||||
|
console.print(f"[yellow]Tip: Run 'docker images' to see available images with prefix '{IMAGE_PREFIX}'[/yellow]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prepare Azure registry image name (remove the prefix since it will be added by the registry)
|
||||||
|
image_name_without_prefix = image_name[len(IMAGE_PREFIX):]
|
||||||
|
azure_image_name = f"{AZURE_REGISTRY}/{image_name_without_prefix}"
|
||||||
|
|
||||||
|
# Show what we're about to do
|
||||||
|
info_panel = Panel.fit(
|
||||||
|
f"[bold]Image:[/bold] {image_name}\n"
|
||||||
|
f"[bold]Target:[/bold] {azure_image_name}\n"
|
||||||
|
f"[bold]Registry:[/bold] {AZURE_REGISTRY}",
|
||||||
|
title="🚀 Push Configuration",
|
||||||
|
border_style="green"
|
||||||
|
)
|
||||||
|
console.print(info_panel)
|
||||||
|
|
||||||
|
# Confirm action unless force is specified
|
||||||
|
if not force:
|
||||||
|
if not Confirm.ask(f"\nProceed with pushing [cyan]{image_name}[/cyan] to Azure?"):
|
||||||
|
console.print("[yellow]Operation cancelled.[/yellow]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
console.print("\n[bold]Starting push process...[/bold]")
|
||||||
|
|
||||||
|
# Tag the image
|
||||||
|
if not run_docker_command(
|
||||||
|
["docker", "tag", image_name, azure_image_name],
|
||||||
|
f"Tagging image as {azure_image_name}"
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Push the image
|
||||||
|
if not run_docker_command(
|
||||||
|
["docker", "push", azure_image_name],
|
||||||
|
f"Pushing {azure_image_name} to registry"
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Success message
|
||||||
|
success_panel = Panel.fit(
|
||||||
|
f"[bold green]✅ Successfully pushed![/bold green]\n"
|
||||||
|
f"[dim]Image: {azure_image_name}[/dim]",
|
||||||
|
title="🎉 Success",
|
||||||
|
border_style="green"
|
||||||
|
)
|
||||||
|
console.print(success_panel)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Push Docker images to Azure Container Registry",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=f"""
|
||||||
|
Examples:
|
||||||
|
python push.py {IMAGE_PREFIX}my-app:latest # Push specific image
|
||||||
|
python push.py --interactive # Interactive mode
|
||||||
|
python push.py {IMAGE_PREFIX}my-app:latest --force # Skip confirmation
|
||||||
|
python push.py --list # List available images
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"image_name",
|
||||||
|
nargs="?",
|
||||||
|
help="Name of the Docker image to push"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--interactive",
|
||||||
|
action="store_true",
|
||||||
|
help="Run in interactive mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip confirmation prompts"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
action="store_true",
|
||||||
|
help="List available Docker images and exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle --list option
|
||||||
|
if args.list:
|
||||||
|
console.print("[bold]Available Docker Images:[/bold]")
|
||||||
|
images = get_available_images()
|
||||||
|
display_available_images(images)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine image name
|
||||||
|
image_name = args.image_name
|
||||||
|
|
||||||
|
# Interactive mode or no image provided
|
||||||
|
if args.interactive or not image_name:
|
||||||
|
image_name = interactive_mode()
|
||||||
|
if not image_name:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Push the image
|
||||||
|
success = push_image(image_name, args.force)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Unexpected error: {e}[/red]")
|
||||||
|
sys.exit(1)
|
||||||
337
.mise/tasks/container/start.py
Executable file
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
#MISE silent=true
|
||||||
|
#MISE description="Run the container for a project"
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["rich"]
|
||||||
|
# ///
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||||
|
from rich.prompt import Prompt, Confirm
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
VAULT_ADDRESS = "https://vault.ia-innovacion.work"
|
||||||
|
DOCKER_REGISTRY = "mayacontigo"
|
||||||
|
DEFAULT_PORT = 9000
|
||||||
|
CONTAINER_PORT = 80
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(command: list[str], description: str, capture_output: bool = True) -> tuple[bool, str]:
|
||||||
|
"""Run a command with rich progress indication."""
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
console=console,
|
||||||
|
transient=True,
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task(description, total=None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=capture_output,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
progress.update(task, description=f"✅ {description}")
|
||||||
|
return True, result.stdout.strip() if capture_output else ""
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
progress.update(task, description=f"❌ {description}")
|
||||||
|
error_msg = e.stderr if capture_output else str(e)
|
||||||
|
console.print(f"[red]Error running command: {' '.join(command)}[/red]")
|
||||||
|
if error_msg:
|
||||||
|
console.print(f"[red]Error output: {error_msg}[/red]")
|
||||||
|
return False, ""
|
||||||
|
except FileNotFoundError:
|
||||||
|
progress.update(task, description=f"❌ {description}")
|
||||||
|
console.print(f"[red]Command not found: {command[0]}. Please ensure it's installed and in your PATH.[/red]")
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault_token() -> Optional[str]:
|
||||||
|
"""Get vault token from Vault CLI."""
|
||||||
|
success, output = run_command(
|
||||||
|
["vault", "token", "lookup", "-format=json", f"-address={VAULT_ADDRESS}"],
|
||||||
|
"Fetching Vault token"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(output)
|
||||||
|
return data.get("data", {}).get("id")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
console.print("[red]Failed to parse Vault token response[/red]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_docker_image_exists(app_name: str) -> bool:
|
||||||
|
"""Check if the Docker image exists locally or can be pulled."""
|
||||||
|
image_name = f"{DOCKER_REGISTRY}/{app_name}"
|
||||||
|
|
||||||
|
# First check if image exists locally
|
||||||
|
success, _ = run_command(
|
||||||
|
["docker", "images", "-q", image_name],
|
||||||
|
f"Checking if {image_name} exists locally"
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If not local, try to pull it
|
||||||
|
console.print(f"[yellow]Image {image_name} not found locally. Attempting to pull...[/yellow]")
|
||||||
|
success, _ = run_command(
|
||||||
|
["docker", "pull", image_name],
|
||||||
|
f"Pulling {image_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_apps() -> list[str]:
|
||||||
|
"""Get list of available Docker images from the registry."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
images = [line.strip() for line in result.stdout.split('\n') if line.strip()]
|
||||||
|
# Filter for banortegpt registry images
|
||||||
|
apps = []
|
||||||
|
for img in images:
|
||||||
|
if img.startswith(f"{DOCKER_REGISTRY}/"):
|
||||||
|
app_name = img.replace(f"{DOCKER_REGISTRY}/", "").split(":")[0]
|
||||||
|
if app_name not in apps:
|
||||||
|
apps.append(app_name)
|
||||||
|
return apps
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def display_available_apps(apps: list[str]) -> None:
|
||||||
|
"""Display available apps in a nice table."""
|
||||||
|
if not apps:
|
||||||
|
console.print("[yellow]No apps found locally.[/yellow]")
|
||||||
|
console.print("[dim]Try pulling an image first or check your Docker registry connection.[/dim]")
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(title="Available Apps")
|
||||||
|
table.add_column("App Name", style="cyan")
|
||||||
|
table.add_column("Image", style="magenta")
|
||||||
|
table.add_column("Status", style="green")
|
||||||
|
|
||||||
|
for app in apps:
|
||||||
|
image = f"{DOCKER_REGISTRY}/{app}"
|
||||||
|
status = "🚀 Ready to run"
|
||||||
|
table.add_row(app, image, status)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_mode() -> Optional[str]:
|
||||||
|
"""Run in interactive mode to select an app."""
|
||||||
|
console.print(Panel.fit(
|
||||||
|
"[bold blue]🐳 Docker Container Starter[/bold blue]\n"
|
||||||
|
"[dim]Interactive mode - Let's select an app to run[/dim]",
|
||||||
|
border_style="blue"
|
||||||
|
))
|
||||||
|
|
||||||
|
# Get available apps
|
||||||
|
console.print("\n[bold]Fetching available apps...[/bold]")
|
||||||
|
apps = get_available_apps()
|
||||||
|
|
||||||
|
if not apps:
|
||||||
|
console.print("[red]No apps found. Please ensure you have Docker images available.[/red]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
display_available_apps(apps)
|
||||||
|
|
||||||
|
# Prompt for app selection
|
||||||
|
console.print("\n[bold]Select an app to run:[/bold]")
|
||||||
|
app_name = Prompt.ask(
|
||||||
|
"Enter the app name (or press Enter to see suggestions)",
|
||||||
|
default="",
|
||||||
|
show_default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not app_name:
|
||||||
|
# Show suggestions
|
||||||
|
console.print("\n[dim]Here are your available apps:[/dim]")
|
||||||
|
for i, app in enumerate(apps, 1):
|
||||||
|
console.print(f" {i}. [cyan]{app}[/cyan]")
|
||||||
|
|
||||||
|
choice = Prompt.ask(
|
||||||
|
"Enter app name or number from suggestions",
|
||||||
|
default="1"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice_num = int(choice)
|
||||||
|
if 1 <= choice_num <= len(apps):
|
||||||
|
app_name = apps[choice_num - 1]
|
||||||
|
else:
|
||||||
|
console.print("[red]Invalid choice.[/red]")
|
||||||
|
return None
|
||||||
|
except ValueError:
|
||||||
|
app_name = choice
|
||||||
|
|
||||||
|
return app_name
|
||||||
|
|
||||||
|
|
||||||
|
def start_container(app_name: str, port: int = DEFAULT_PORT, force: bool = False) -> bool:
|
||||||
|
"""Start a Docker container for the specified app."""
|
||||||
|
# Validate app name
|
||||||
|
if not app_name:
|
||||||
|
console.print("[red]Error: App name cannot be empty[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if image exists
|
||||||
|
if not check_docker_image_exists(app_name):
|
||||||
|
console.print(f"[red]Error: Image for app '{app_name}' not found[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get vault token
|
||||||
|
console.print("\n[bold]Getting Vault token...[/bold]")
|
||||||
|
vault_token = get_vault_token()
|
||||||
|
if not vault_token:
|
||||||
|
console.print("[red]Failed to get Vault token[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prepare container configuration
|
||||||
|
image_name = f"{DOCKER_REGISTRY}/{app_name}"
|
||||||
|
|
||||||
|
# Show what we're about to do
|
||||||
|
info_panel = Panel.fit(
|
||||||
|
f"[bold]App:[/bold] {app_name}\n"
|
||||||
|
f"[bold]Image:[/bold] {image_name}\n"
|
||||||
|
f"[bold]Port:[/bold] {port}:{CONTAINER_PORT}\n"
|
||||||
|
f"[bold]Vault:[/bold] {VAULT_ADDRESS}\n"
|
||||||
|
f"[bold]Token:[/bold] {'*' * 8}...{vault_token[-4:] if len(vault_token) > 8 else '****'}",
|
||||||
|
title="🚀 Container Configuration",
|
||||||
|
border_style="green"
|
||||||
|
)
|
||||||
|
console.print(info_panel)
|
||||||
|
|
||||||
|
# Confirm action unless force is specified
|
||||||
|
if not force:
|
||||||
|
if not Confirm.ask(f"\nStart container for [cyan]{app_name}[/cyan] on port {port}?"):
|
||||||
|
console.print("[yellow]Operation cancelled.[/yellow]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
console.print("\n[bold]Starting container...[/bold]")
|
||||||
|
|
||||||
|
# Build docker run command
|
||||||
|
docker_cmd = [
|
||||||
|
"docker", "run",
|
||||||
|
"-p", f"{port}:{CONTAINER_PORT}",
|
||||||
|
"--rm",
|
||||||
|
"-it",
|
||||||
|
"-e", f"VAULT_TOKEN={vault_token}",
|
||||||
|
image_name
|
||||||
|
]
|
||||||
|
|
||||||
|
# Run the container (this will be interactive)
|
||||||
|
console.print(f"[green]Running: {' '.join(docker_cmd[:7])}... {image_name}[/green]")
|
||||||
|
console.print(f"[dim]Container will be available at: http://localhost:{port}[/dim]")
|
||||||
|
console.print("[dim]Press Ctrl+C to stop the container[/dim]\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(docker_cmd, check=True)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
console.print(f"[red]Container failed to start: {e}[/red]")
|
||||||
|
return False
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Container stopped by user.[/yellow]")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Start Docker containers for Banorte GPT apps",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
python start.py my-app # Start specific app
|
||||||
|
python start.py --interactive # Interactive mode
|
||||||
|
python start.py my-app --port 8080 # Custom port
|
||||||
|
python start.py my-app --force # Skip confirmation
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"app_name",
|
||||||
|
nargs="?",
|
||||||
|
help="Name of the app to start"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--interactive",
|
||||||
|
action="store_true",
|
||||||
|
help="Run in interactive mode"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--port",
|
||||||
|
type=int,
|
||||||
|
default=DEFAULT_PORT,
|
||||||
|
help=f"Port to bind to (default: {DEFAULT_PORT})"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip confirmation prompts"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
action="store_true",
|
||||||
|
help="List available apps and exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle --list option
|
||||||
|
if args.list:
|
||||||
|
console.print("[bold]Available Apps:[/bold]")
|
||||||
|
apps = get_available_apps()
|
||||||
|
display_available_apps(apps)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine app name
|
||||||
|
app_name = args.app_name
|
||||||
|
|
||||||
|
# Interactive mode or no app provided
|
||||||
|
if args.interactive or not app_name:
|
||||||
|
app_name = interactive_mode()
|
||||||
|
if not app_name:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Start the container
|
||||||
|
success = start_container(app_name, args.port, args.force)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Unexpected error: {e}[/red]")
|
||||||
|
sys.exit(1)
|
||||||
582
.mise/tasks/dev.py
Executable file
@@ -0,0 +1,582 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
#MISE silent=true
|
||||||
|
#MISE description="Start the development environment for a project"
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["rich"]
|
||||||
|
# ///
|
||||||
|
import argparse
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.live import Live
|
||||||
|
from rich.layout import Layout
|
||||||
|
from rich.status import Status
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DEFAULT_FRONTEND_CMD = ["pnpm", "run", "dev"]
|
||||||
|
DEFAULT_BACKEND_CMD = ["uv", "run", "uvicorn", "api.server:app", "--reload"]
|
||||||
|
DEFAULT_FRONTEND_PORT = 3000
|
||||||
|
DEFAULT_BACKEND_PORT = 8000
|
||||||
|
|
||||||
|
class ProcessManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.processes: Dict[str, subprocess.Popen] = {}
|
||||||
|
self.running = False
|
||||||
|
self.logs: Dict[str, List[str]] = {"frontend": [], "backend": []}
|
||||||
|
|
||||||
|
def start_process(self, name: str, cmd: List[str], cwd: Optional[str] = None) -> bool:
|
||||||
|
"""Start a process and track it."""
|
||||||
|
try:
|
||||||
|
console.print(f"[dim]Starting {name} in {cwd or 'current directory'}: {' '.join(cmd)}[/dim]")
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
universal_newlines=True,
|
||||||
|
cwd=cwd
|
||||||
|
)
|
||||||
|
self.processes[name] = process
|
||||||
|
|
||||||
|
# Start thread to capture output
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._capture_output,
|
||||||
|
args=(name, process),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Failed to start {name}: {e}[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _capture_output(self, name: str, process: subprocess.Popen):
|
||||||
|
"""Capture process output in a separate thread."""
|
||||||
|
while True:
|
||||||
|
stdout = process.stdout
|
||||||
|
assert stdout is not None
|
||||||
|
output = stdout.readline()
|
||||||
|
if output == '' and process.poll() is not None:
|
||||||
|
break
|
||||||
|
if output:
|
||||||
|
# Keep only last 50 lines
|
||||||
|
self.logs[name].append(output.strip())
|
||||||
|
if len(self.logs[name]) > 50:
|
||||||
|
self.logs[name].pop(0)
|
||||||
|
|
||||||
|
def stop_all(self):
|
||||||
|
"""Stop all processes."""
|
||||||
|
self.running = False
|
||||||
|
for name, process in self.processes.items():
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_running(self) -> bool:
|
||||||
|
"""Check if any process is still running."""
|
||||||
|
return any(p.poll() is None for p in self.processes.values())
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, str]:
|
||||||
|
"""Get status of all processes."""
|
||||||
|
status = {}
|
||||||
|
for name, process in self.processes.items():
|
||||||
|
if process.poll() is None:
|
||||||
|
status[name] = "🟢 Running"
|
||||||
|
else:
|
||||||
|
status[name] = f"🔴 Stopped (exit code: {process.poll()})"
|
||||||
|
return status
|
||||||
|
|
||||||
|
def find_project_root() -> Optional[Path]:
|
||||||
|
"""Find the project root directory by looking for package.json."""
|
||||||
|
current_dir = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# Start from the script directory and go up
|
||||||
|
while current_dir != current_dir.parent:
|
||||||
|
if (current_dir / "package.json").exists():
|
||||||
|
return current_dir
|
||||||
|
current_dir = current_dir.parent
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_available_apps(project_root: Path) -> List[str]:
|
||||||
|
"""Get list of available apps in the monorepo."""
|
||||||
|
apps_dir = project_root / "apps"
|
||||||
|
if not apps_dir.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
apps = []
|
||||||
|
for app_dir in apps_dir.iterdir():
|
||||||
|
if app_dir.is_dir() and (app_dir / "package.json").exists():
|
||||||
|
apps.append(app_dir.name)
|
||||||
|
|
||||||
|
return sorted(apps)
|
||||||
|
|
||||||
|
def get_package_name(app_path: Path) -> str:
|
||||||
|
"""Get the package name from package.json."""
|
||||||
|
try:
|
||||||
|
with open(app_path / "package.json", 'r') as f:
|
||||||
|
package_data = json.load(f)
|
||||||
|
return package_data.get("name", "")
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def check_dependencies(project_root: Path, app_name: str | None = None) -> Dict[str, bool]:
|
||||||
|
"""Check if required dependencies are available."""
|
||||||
|
deps = {}
|
||||||
|
|
||||||
|
# Check pnpm global availability
|
||||||
|
try:
|
||||||
|
subprocess.run(["pnpm", "--version"], capture_output=True, check=True)
|
||||||
|
deps["pnpm"] = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
deps["pnpm"] = False
|
||||||
|
|
||||||
|
# Check uv global availability
|
||||||
|
try:
|
||||||
|
subprocess.run(["uv", "--version"], capture_output=True, check=True)
|
||||||
|
deps["uv"] = True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
deps["uv"] = False
|
||||||
|
|
||||||
|
# Check project structure
|
||||||
|
deps["package.json"] = (project_root / "package.json").exists()
|
||||||
|
deps["apps directory"] = (project_root / "apps").exists()
|
||||||
|
|
||||||
|
# If app is specified, check app-specific dependencies
|
||||||
|
if app_name:
|
||||||
|
app_path = project_root / "apps" / app_name
|
||||||
|
|
||||||
|
# Check pnpm workspace install for specific app
|
||||||
|
if deps["pnpm"] and (app_path / "package.json").exists():
|
||||||
|
try:
|
||||||
|
# Use pnpm list to check if the workspace package is valid
|
||||||
|
subprocess.run(
|
||||||
|
["pnpm", "list", "--filter", app_name, "--depth", "0"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
cwd=str(project_root),
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
deps[f"pnpm install ({app_name})"] = True
|
||||||
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||||
|
deps[f"pnpm install ({app_name})"] = False
|
||||||
|
else:
|
||||||
|
deps[f"pnpm install ({app_name})"] = False
|
||||||
|
|
||||||
|
# Check uv sync for specific app
|
||||||
|
if deps["uv"] and (app_path / "api").exists():
|
||||||
|
try:
|
||||||
|
# Check if we can sync the package (dry-run)
|
||||||
|
subprocess.run(
|
||||||
|
["uv", "sync", "--package", app_name, "--dry-run"],
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
cwd=str(project_root),
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
deps[f"uv sync ({app_name})"] = True
|
||||||
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||||
|
deps[f"uv sync ({app_name})"] = False
|
||||||
|
else:
|
||||||
|
deps[f"uv sync ({app_name})"] = False
|
||||||
|
|
||||||
|
return deps
|
||||||
|
|
||||||
|
def display_dependency_status(deps: Dict[str, bool]) -> bool:
|
||||||
|
"""Display dependency status and return if all are satisfied."""
|
||||||
|
table = Table(title="🔍 Dependency Check")
|
||||||
|
table.add_column("Dependency", style="cyan")
|
||||||
|
table.add_column("Status", style="bold")
|
||||||
|
table.add_column("Description", style="dim")
|
||||||
|
|
||||||
|
descriptions = {
|
||||||
|
"pnpm": "Node.js package manager",
|
||||||
|
"uv": "Python package manager",
|
||||||
|
"package.json": "Root project configuration",
|
||||||
|
"apps directory": "Apps directory for monorepo"
|
||||||
|
}
|
||||||
|
|
||||||
|
all_good = True
|
||||||
|
for dep, available in deps.items():
|
||||||
|
if available:
|
||||||
|
status = "[green]✅ Available[/green]"
|
||||||
|
else:
|
||||||
|
status = "[red]❌ Missing[/red]"
|
||||||
|
all_good = False
|
||||||
|
|
||||||
|
# Generate description for app-specific dependencies
|
||||||
|
description = descriptions.get(dep, "")
|
||||||
|
if "pnpm install" in dep:
|
||||||
|
description = "Frontend dependencies installation"
|
||||||
|
elif "uv sync" in dep:
|
||||||
|
description = "Backend dependencies synchronization"
|
||||||
|
|
||||||
|
table.add_row(dep, status, description)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
return all_good
|
||||||
|
|
||||||
|
def display_available_apps(apps: List[str], project_root: Path, show_numbers: bool = False):
|
||||||
|
"""Display available apps."""
|
||||||
|
if not apps:
|
||||||
|
console.print("[yellow]No apps found in the monorepo.[/yellow]")
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(title="📱 Available Apps")
|
||||||
|
|
||||||
|
if show_numbers:
|
||||||
|
table.add_column("No.", style="bold yellow", width=4)
|
||||||
|
|
||||||
|
table.add_column("App Name", style="cyan")
|
||||||
|
table.add_column("Frontend", style="green")
|
||||||
|
table.add_column("Backend", style="blue")
|
||||||
|
|
||||||
|
for i, app in enumerate(apps, 1):
|
||||||
|
app_path = project_root / "apps" / app
|
||||||
|
has_frontend = (app_path / "package.json").exists()
|
||||||
|
has_backend = (app_path / "api").exists()
|
||||||
|
|
||||||
|
frontend_status = "✅" if has_frontend else "❌"
|
||||||
|
backend_status = "✅" if has_backend else "❌"
|
||||||
|
|
||||||
|
if show_numbers:
|
||||||
|
table.add_row(str(i), app, frontend_status, backend_status)
|
||||||
|
else:
|
||||||
|
table.add_row(app, frontend_status, backend_status)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
def interactive_select_app(available_apps: List[str], project_root: Path) -> Optional[str]:
|
||||||
|
"""Interactively select an app from the available apps."""
|
||||||
|
if not available_apps:
|
||||||
|
console.print("[red]❌ No apps found in the monorepo.[/red]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
console.print("\n[bold]Available apps in monorepo:[/bold]")
|
||||||
|
display_available_apps(available_apps, project_root, show_numbers=True)
|
||||||
|
|
||||||
|
# Create choices with numbers
|
||||||
|
choices = [str(i) for i in range(1, len(available_apps) + 1)]
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
choice = Prompt.ask(
|
||||||
|
"Select an app to start",
|
||||||
|
choices=choices,
|
||||||
|
default="1"
|
||||||
|
)
|
||||||
|
|
||||||
|
selected_app = available_apps[int(choice) - 1]
|
||||||
|
console.print(f"[green]✅ Selected app: {selected_app}[/green]")
|
||||||
|
return selected_app
|
||||||
|
|
||||||
|
except (ValueError, IndexError, KeyboardInterrupt):
|
||||||
|
console.print("[yellow]Selection cancelled.[/yellow]")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_live_dashboard(manager: ProcessManager, app_name: str) -> Layout:
|
||||||
|
"""Create a live dashboard layout."""
|
||||||
|
layout = Layout()
|
||||||
|
|
||||||
|
layout.split_column(
|
||||||
|
Layout(name="header", size=3),
|
||||||
|
Layout(name="body"),
|
||||||
|
Layout(name="footer", size=3)
|
||||||
|
)
|
||||||
|
|
||||||
|
layout["body"].split_row(
|
||||||
|
Layout(name="frontend"),
|
||||||
|
Layout(name="backend")
|
||||||
|
)
|
||||||
|
|
||||||
|
return layout
|
||||||
|
|
||||||
|
def update_dashboard(layout: Layout, manager: ProcessManager, app_name: str):
|
||||||
|
"""Update the dashboard with current status."""
|
||||||
|
# Header
|
||||||
|
layout["header"].update(
|
||||||
|
Panel(
|
||||||
|
f"[bold blue]🚀 Development Environment - {app_name}[/bold blue]\n"
|
||||||
|
"[dim]Press Ctrl+C to stop all services[/dim]",
|
||||||
|
border_style="blue"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
status = manager.get_status()
|
||||||
|
|
||||||
|
# Frontend panel
|
||||||
|
frontend_logs = "\n".join(manager.logs.get("frontend", [])[-10:]) # Last 10 lines
|
||||||
|
frontend_status = status.get("frontend", "🔴 Not started")
|
||||||
|
|
||||||
|
layout["frontend"].update(
|
||||||
|
Panel(
|
||||||
|
f"[bold]Status:[/bold] {frontend_status}\n\n"
|
||||||
|
f"[dim]Recent output:[/dim]\n{frontend_logs}",
|
||||||
|
title="🎨 Frontend (pnpm)",
|
||||||
|
border_style="green" if "Running" in frontend_status else "red"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backend panel
|
||||||
|
backend_logs = "\n".join(manager.logs.get("backend", [])[-10:]) # Last 10 lines
|
||||||
|
backend_status = status.get("backend", "🔴 Not started")
|
||||||
|
|
||||||
|
layout["backend"].update(
|
||||||
|
Panel(
|
||||||
|
f"[bold]Status:[/bold] {backend_status}\n\n"
|
||||||
|
f"[dim]Recent output:[/dim]\n{backend_logs}",
|
||||||
|
title="🔧 Backend (uvicorn)",
|
||||||
|
border_style="green" if "Running" in backend_status else "red"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
layout["footer"].update(
|
||||||
|
Panel(
|
||||||
|
f"[bold]Frontend:[/bold] http://localhost:{DEFAULT_FRONTEND_PORT} | "
|
||||||
|
f"[bold]Backend:[/bold] http://localhost:{DEFAULT_BACKEND_PORT}",
|
||||||
|
border_style="blue"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_development_environment(
|
||||||
|
app_name: str,
|
||||||
|
frontend_cmd: List[str],
|
||||||
|
backend_cmd: List[str],
|
||||||
|
show_dashboard: bool = True,
|
||||||
|
project_root: Path | None = None
|
||||||
|
) -> bool:
|
||||||
|
"""Start the development environment."""
|
||||||
|
manager = ProcessManager()
|
||||||
|
|
||||||
|
# Set up signal handler for graceful shutdown
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
console.print(f"\n[yellow]Shutting down development environment for {app_name}...[/yellow]")
|
||||||
|
manager.stop_all()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
assert project_root is not None, "project_root must be provided"
|
||||||
|
|
||||||
|
# Check dependencies first
|
||||||
|
console.print("[bold]Checking dependencies...[/bold]")
|
||||||
|
deps = check_dependencies(project_root, app_name)
|
||||||
|
|
||||||
|
if not display_dependency_status(deps):
|
||||||
|
console.print("\n[red]❌ Some dependencies are missing. Please install them first.[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
console.print("\n[green]✅ All dependencies are available![/green]")
|
||||||
|
|
||||||
|
# Set up paths
|
||||||
|
app_path = project_root / "apps" / app_name
|
||||||
|
frontend_cwd = str(app_path) if (app_path / "package.json").exists() else None
|
||||||
|
backend_cwd = str(app_path) if (app_path / "api").exists() else None
|
||||||
|
|
||||||
|
# Show startup configuration
|
||||||
|
config_panel = Panel.fit(
|
||||||
|
f"[bold]App:[/bold] {app_name}\n"
|
||||||
|
f"[bold]Frontend:[/bold] {' '.join(frontend_cmd)} (cwd: {frontend_cwd or 'N/A'})\n"
|
||||||
|
f"[bold]Backend:[/bold] {' '.join(backend_cmd)} (cwd: {backend_cwd or 'N/A'})\n"
|
||||||
|
f"[bold]Frontend URL:[/bold] http://localhost:{DEFAULT_FRONTEND_PORT}\n"
|
||||||
|
f"[bold]Backend URL:[/bold] http://localhost:{DEFAULT_BACKEND_PORT}",
|
||||||
|
title="🚀 Development Configuration",
|
||||||
|
border_style="green"
|
||||||
|
)
|
||||||
|
console.print(config_panel)
|
||||||
|
|
||||||
|
# Start processes
|
||||||
|
console.print("\n[bold]Starting development servers...[/bold]")
|
||||||
|
|
||||||
|
# Start frontend if available
|
||||||
|
if frontend_cwd:
|
||||||
|
with Status("Starting frontend server...", spinner="dots"):
|
||||||
|
if not manager.start_process("frontend", frontend_cmd, frontend_cwd):
|
||||||
|
console.print("[red]Failed to start frontend[/red]")
|
||||||
|
return False
|
||||||
|
time.sleep(2) # Give it a moment to start
|
||||||
|
else:
|
||||||
|
console.print("[yellow]No frontend found for this app[/yellow]")
|
||||||
|
|
||||||
|
# Start backend if available
|
||||||
|
if backend_cwd:
|
||||||
|
with Status("Starting backend server...", spinner="dots"):
|
||||||
|
if not manager.start_process("backend", backend_cmd, backend_cwd):
|
||||||
|
console.print("[red]Failed to start backend[/red]")
|
||||||
|
return False
|
||||||
|
time.sleep(2) # Give it a moment to start
|
||||||
|
else:
|
||||||
|
console.print("[yellow]No backend found for this app[/yellow]")
|
||||||
|
|
||||||
|
if not manager.processes:
|
||||||
|
console.print("[red]No services to start for this app[/red]")
|
||||||
|
return False
|
||||||
|
|
||||||
|
manager.running = True
|
||||||
|
|
||||||
|
if show_dashboard:
|
||||||
|
# Live dashboard
|
||||||
|
layout = create_live_dashboard(manager, app_name)
|
||||||
|
|
||||||
|
with Live(layout, refresh_per_second=2, screen=True):
|
||||||
|
try:
|
||||||
|
while manager.is_running():
|
||||||
|
update_dashboard(layout, manager, app_name)
|
||||||
|
time.sleep(0.5)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Simple mode - just wait
|
||||||
|
try:
|
||||||
|
console.print(f"\n[green]✅ Development environment started for {app_name}![/green]")
|
||||||
|
console.print("[dim]Press Ctrl+C to stop all services[/dim]")
|
||||||
|
|
||||||
|
while manager.is_running():
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
console.print(f"\n[yellow]Stopping all services for {app_name}...[/yellow]")
|
||||||
|
manager.stop_all()
|
||||||
|
console.print("[green]✅ Development environment stopped.[/green]")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Find project root
|
||||||
|
project_root = find_project_root()
|
||||||
|
if not project_root:
|
||||||
|
console.print("[red]❌ Could not find project root (package.json not found)[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
console.print(f"[dim]Project root: {project_root}[/dim]")
|
||||||
|
|
||||||
|
# Get available apps
|
||||||
|
available_apps = get_available_apps(project_root)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Start development environment with frontend and backend servers for a monorepo app",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=f"""
|
||||||
|
Available apps: {', '.join(available_apps) if available_apps else 'None found'}
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python dev.py # Interactive app selection
|
||||||
|
python dev.py --app bursatil # Start bursatil app directly
|
||||||
|
python dev.py --app ocp --no-dashboard # Start ocp app without dashboard
|
||||||
|
python dev.py --list-apps # List available apps
|
||||||
|
python dev.py --app bursatil --frontend-cmd "pnpm run build" # Custom frontend command
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--app",
|
||||||
|
choices=available_apps,
|
||||||
|
help="App to start (if not provided, you'll be prompted to select one)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--frontend-cmd",
|
||||||
|
default=" ".join(DEFAULT_FRONTEND_CMD),
|
||||||
|
help="Frontend command to run (default: pnpm run dev)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--backend-cmd",
|
||||||
|
default=" ".join(DEFAULT_BACKEND_CMD),
|
||||||
|
help="Backend command to run (default: uv run uvicorn api.server:app --reload)"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-dashboard",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable the live dashboard"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--check-deps",
|
||||||
|
action="store_true",
|
||||||
|
help="Only check dependencies and exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-apps",
|
||||||
|
action="store_true",
|
||||||
|
help="List available apps and exit"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Handle --check-deps
|
||||||
|
if args.check_deps:
|
||||||
|
console.print("[bold]Checking dependencies...[/bold]")
|
||||||
|
deps = check_dependencies(project_root)
|
||||||
|
display_dependency_status(deps)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle --list-apps
|
||||||
|
if args.list_apps:
|
||||||
|
console.print("[bold]Available apps in monorepo:[/bold]")
|
||||||
|
display_available_apps(available_apps, project_root)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle app selection
|
||||||
|
if not args.app:
|
||||||
|
if not available_apps:
|
||||||
|
console.print("[red]❌ No apps found in the monorepo.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Interactive selection
|
||||||
|
selected_app = interactive_select_app(available_apps, project_root)
|
||||||
|
if not selected_app:
|
||||||
|
sys.exit(1)
|
||||||
|
args.app = selected_app
|
||||||
|
|
||||||
|
# Parse commands
|
||||||
|
frontend_cmd = args.frontend_cmd.split()
|
||||||
|
backend_cmd = args.backend_cmd.split()
|
||||||
|
show_dashboard = not args.no_dashboard
|
||||||
|
|
||||||
|
# Start development environment
|
||||||
|
success = start_development_environment(
|
||||||
|
args.app,
|
||||||
|
frontend_cmd,
|
||||||
|
backend_cmd,
|
||||||
|
show_dashboard,
|
||||||
|
project_root
|
||||||
|
)
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"[red]Unexpected error: {e}[/red]")
|
||||||
|
sys.exit(1)
|
||||||
326
.mise/tasks/new.py
Executable file
@@ -0,0 +1,326 @@
|
|||||||
|
#!/usr/bin/env -S uv run --script
|
||||||
|
#MISE silent=true
|
||||||
|
#MISE description="Create a new project from our templates"
|
||||||
|
# /// script
|
||||||
|
# dependencies = ["rich", "pyyaml"]
|
||||||
|
# ///
|
||||||
|
"""
|
||||||
|
Slick app generator script with Rich formatting
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from rich.table import Table
|
||||||
|
from rich import box
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
def show_banner():
|
||||||
|
"""Display a stylish banner"""
|
||||||
|
banner = Text("🚀 RAG TEMPLATE", style="bold magenta")
|
||||||
|
banner.stylize("bold cyan", 0, 4) # Style the rocket emoji
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
banner,
|
||||||
|
box=box.DOUBLE,
|
||||||
|
border_style="bright_blue",
|
||||||
|
padding=(1, 2)
|
||||||
|
)
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
def validate_app_name(app_name):
|
||||||
|
"""Validate the app name"""
|
||||||
|
if not app_name:
|
||||||
|
return False, "App name cannot be empty"
|
||||||
|
|
||||||
|
if not app_name.replace('_', '').replace('-', '').isalnum():
|
||||||
|
return False, "App name can only contain letters, numbers, hyphens, and underscores"
|
||||||
|
|
||||||
|
if len(app_name) < 2:
|
||||||
|
return False, "App name must be at least 2 characters long"
|
||||||
|
|
||||||
|
if len(app_name) > 50:
|
||||||
|
return False, "App name must be less than 50 characters long"
|
||||||
|
|
||||||
|
return True, "Valid app name"
|
||||||
|
|
||||||
|
def run_command(command, description, cwd=None):
|
||||||
|
"""Run a command with progress indicator"""
|
||||||
|
try:
|
||||||
|
with Progress(
|
||||||
|
SpinnerColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
BarColumn(),
|
||||||
|
TimeElapsedColumn(),
|
||||||
|
console=console,
|
||||||
|
transient=True
|
||||||
|
) as progress:
|
||||||
|
task = progress.add_task(description, total=None)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
progress.update(task, completed=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
console.print(f"❌ [red]Error running command:[/red] {command}")
|
||||||
|
console.print(f"[red]Error output:[/red] {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ [red]Exception occurred:[/red] {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
class CustomYAMLDumper(yaml.SafeDumper):
|
||||||
|
"""Custom YAML dumper that preserves list formatting"""
|
||||||
|
|
||||||
|
def write_line_break(self, data=None):
|
||||||
|
super().write_line_break(data)
|
||||||
|
if len(self.indents) == 1:
|
||||||
|
super().write_line_break()
|
||||||
|
|
||||||
|
def custom_yaml_dump(data, stream=None):
|
||||||
|
"""Custom YAML dump function that maintains proper list formatting"""
|
||||||
|
class ListDumper(yaml.SafeDumper):
|
||||||
|
def increase_indent(self, flow=False, indentless=False):
|
||||||
|
return super().increase_indent(flow, False)
|
||||||
|
|
||||||
|
def represent_list(self, data):
|
||||||
|
return self.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=False)
|
||||||
|
|
||||||
|
def represent_dict(self, data):
|
||||||
|
return self.represent_mapping('tag:yaml.org,2002:map', data, flow_style=False)
|
||||||
|
|
||||||
|
ListDumper.add_representer(list, ListDumper.represent_list)
|
||||||
|
ListDumper.add_representer(dict, ListDumper.represent_dict)
|
||||||
|
|
||||||
|
return yaml.dump(data, stream=stream, Dumper=ListDumper,
|
||||||
|
default_flow_style=False, indent=2, sort_keys=False,
|
||||||
|
allow_unicode=True, width=1000)
|
||||||
|
|
||||||
|
def find_available_port(compose_path):
|
||||||
|
"""Find the next available port for the new service"""
|
||||||
|
used_ports = set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(compose_path, 'r') as f:
|
||||||
|
compose_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Extract used ports from existing services
|
||||||
|
services = compose_data.get('services', {})
|
||||||
|
for service_name, service_config in services.items():
|
||||||
|
if service_name == 'traefik': # Skip traefik
|
||||||
|
continue
|
||||||
|
ports = service_config.get('ports', [])
|
||||||
|
for port_mapping in ports:
|
||||||
|
if isinstance(port_mapping, str):
|
||||||
|
external_port = int(port_mapping.split(':')[0])
|
||||||
|
used_ports.add(external_port)
|
||||||
|
|
||||||
|
# Find next available port starting from 8001
|
||||||
|
port = 8001
|
||||||
|
while port in used_ports:
|
||||||
|
port += 1
|
||||||
|
|
||||||
|
return port
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"⚠️ [yellow]Could not determine available port, using 8001:[/yellow] {str(e)}")
|
||||||
|
return 8001
|
||||||
|
|
||||||
|
def generate_service_config(app_name, port):
|
||||||
|
"""Generate Docker Compose service configuration for new app"""
|
||||||
|
return {
|
||||||
|
'image': f'mayacontigo/{app_name}:latest',
|
||||||
|
'build': {
|
||||||
|
'context': '.',
|
||||||
|
'dockerfile': '.containers/unit/Dockerfile',
|
||||||
|
'args': {
|
||||||
|
'PACKAGE': app_name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'x-bake': {
|
||||||
|
'tags': [f'mayacontigo/{app_name}:latest']
|
||||||
|
},
|
||||||
|
'ports': [f'{port}:80'],
|
||||||
|
'labels': [
|
||||||
|
'traefik.enable=true',
|
||||||
|
f'traefik.http.routers.{app_name}.rule=PathPrefix(`/api/maya{app_name}`)',
|
||||||
|
f'traefik.http.routers.{app_name}.entrypoints=web',
|
||||||
|
f'traefik.http.routers.{app_name}.middlewares={app_name}-strip',
|
||||||
|
f'traefik.http.middlewares.{app_name}-strip.stripprefix.prefixes=/api/maya{app_name}'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def add_service_to_compose(app_name):
|
||||||
|
"""Add new service to compose.yaml file"""
|
||||||
|
compose_path = Path('compose.yaml')
|
||||||
|
|
||||||
|
if not compose_path.exists():
|
||||||
|
console.print(f"⚠️ [yellow]compose.yaml not found, skipping service addition.[/yellow]")
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create backup
|
||||||
|
backup_path = compose_path.with_suffix('.yaml.backup')
|
||||||
|
shutil.copy2(compose_path, backup_path)
|
||||||
|
|
||||||
|
# Load existing compose file
|
||||||
|
with open(compose_path, 'r') as f:
|
||||||
|
compose_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# Check if service already exists
|
||||||
|
services = compose_data.get('services', {})
|
||||||
|
if app_name in services:
|
||||||
|
console.print(f"⚠️ [yellow]Service '{app_name}' already exists in compose.yaml[/yellow]")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Find available port
|
||||||
|
port = find_available_port(compose_path)
|
||||||
|
|
||||||
|
# Generate service configuration
|
||||||
|
service_config = generate_service_config(app_name, port)
|
||||||
|
|
||||||
|
# Add service to compose data
|
||||||
|
services[app_name] = service_config
|
||||||
|
|
||||||
|
# Write updated compose file with proper formatting
|
||||||
|
with open(compose_path, 'w') as f:
|
||||||
|
custom_yaml_dump(compose_data, f)
|
||||||
|
|
||||||
|
console.print(f"✅ [green]Added service '{app_name}' to compose.yaml (port {port})[/green]")
|
||||||
|
|
||||||
|
# Remove backup on success
|
||||||
|
backup_path.unlink()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"❌ [red]Failed to add service to compose.yaml:[/red] {str(e)}")
|
||||||
|
|
||||||
|
# Restore backup if it exists
|
||||||
|
if backup_path.exists():
|
||||||
|
shutil.copy2(backup_path, compose_path)
|
||||||
|
backup_path.unlink()
|
||||||
|
console.print("🔄 [yellow]Restored original compose.yaml from backup[/yellow]")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def show_summary(app_name):
|
||||||
|
"""Show a summary of what was created"""
|
||||||
|
table = Table(title="🎉 App Created Successfully!", box=box.ROUNDED)
|
||||||
|
table.add_column("Component", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("Status", style="green")
|
||||||
|
table.add_column("Location", style="yellow")
|
||||||
|
|
||||||
|
table.add_row("📁 Project Directory", "✅ Created", f"./apps/{app_name}")
|
||||||
|
table.add_row("📦 Dependencies", "✅ Installed", "package.json + requirements.txt")
|
||||||
|
table.add_row("🔧 Template", "✅ Applied", "RAG template")
|
||||||
|
table.add_row("🐍 Python Environment", "✅ Synced", "uv environment")
|
||||||
|
table.add_row("🐳 Docker Service", "✅ Added", "compose.yaml")
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
# Next steps
|
||||||
|
next_steps = Panel(
|
||||||
|
f"[bold green]Next Steps:[/bold green]\n\n"
|
||||||
|
f"1. [cyan]mise dev --app {app_name}[/cyan]\n"
|
||||||
|
f"2. [cyan]Start building your RAG app! 🚀[/cyan]",
|
||||||
|
title="🎯 What's Next?",
|
||||||
|
border_style="green",
|
||||||
|
box=box.ROUNDED
|
||||||
|
)
|
||||||
|
console.print(next_steps)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function"""
|
||||||
|
show_banner()
|
||||||
|
|
||||||
|
# Get app name from command line or prompt
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
console.print("[yellow]No app name provided as argument.[/yellow]")
|
||||||
|
app_name = Prompt.ask("Enter your app name", default="my-rag-app")
|
||||||
|
else:
|
||||||
|
app_name = sys.argv[1]
|
||||||
|
|
||||||
|
# Validate app name
|
||||||
|
is_valid, message = validate_app_name(app_name)
|
||||||
|
if not is_valid:
|
||||||
|
console.print(f"❌ [red]Invalid app name:[/red] {message}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Show what we're about to do
|
||||||
|
console.print(f"[bold]Creating app:[/bold] [cyan]{app_name}[/cyan]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
# Check if directory already exists
|
||||||
|
app_path = Path(f"apps/{app_name}")
|
||||||
|
if app_path.exists():
|
||||||
|
console.print(f"⚠️ [yellow]Directory 'apps/{app_name}' already exists![/yellow]")
|
||||||
|
if not Prompt.ask("Do you want to continue?", choices=["y", "n"], default="n") == "y":
|
||||||
|
console.print("Operation cancelled.")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Step 1: Copy template
|
||||||
|
console.print("📂 [bold]Step 1:[/bold] Copying RAG template...")
|
||||||
|
if not run_command(
|
||||||
|
f"uvx copier copy .templates/rag apps --data project_name={app_name} --trust",
|
||||||
|
"Copying template files..."
|
||||||
|
):
|
||||||
|
console.print("❌ [red]Failed to copy template. Make sure you're in the right directory.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 2: Install npm dependencies
|
||||||
|
console.print("📦 [bold]Step 2:[/bold] Installing npm dependencies...")
|
||||||
|
if not run_command(
|
||||||
|
"pnpm install",
|
||||||
|
"Installing npm packages...",
|
||||||
|
cwd=f"apps/{app_name}"
|
||||||
|
):
|
||||||
|
console.print("❌ [red]Failed to install npm dependencies.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 3: Sync Python environment
|
||||||
|
console.print("🐍 [bold]Step 3:[/bold] Setting up Python environment...")
|
||||||
|
if not run_command(
|
||||||
|
"uv sync",
|
||||||
|
"Syncing Python environment...",
|
||||||
|
cwd=f"apps/{app_name}"
|
||||||
|
):
|
||||||
|
console.print("❌ [red]Failed to sync Python environment.[/red]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Step 4: Add service to compose.yaml
|
||||||
|
console.print("🐳 [bold]Step 4:[/bold] Adding service to compose.yaml...")
|
||||||
|
if not add_service_to_compose(app_name):
|
||||||
|
console.print("❌ [red]Failed to add service to compose.yaml.[/red]")
|
||||||
|
console.print("⚠️ [yellow]You can manually add the service later.[/yellow]")
|
||||||
|
# Don't exit here, this is not critical
|
||||||
|
|
||||||
|
# Success!
|
||||||
|
console.print()
|
||||||
|
show_summary(app_name)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
console.print(f"\n❌ [red]Unexpected error:[/red] {str(e)}")
|
||||||
|
sys.exit(1)
|
||||||
3
.templates/rag/copier.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
project_name:
|
||||||
|
type: str
|
||||||
|
help: Cual es el nombre del proyecto?
|
||||||
18
.templates/rag/{{project_name}}/.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
.templates/rag/{{project_name}}/api/__init__.py
Normal file
3
.templates/rag/{{project_name}}/api/agent/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .main import Agent
|
||||||
|
|
||||||
|
__all__ = ["Agent"]
|
||||||
67
.templates/rag/{{project_name}}/api/agent/main.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, AsyncGenerator
|
||||||
|
|
||||||
|
from langchain_core.messages import AIMessageChunk
|
||||||
|
from langchain_azure_ai.chat_models import AzureAIChatCompletionsModel
|
||||||
|
|
||||||
|
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 Agent:
|
||||||
|
system_prompt = SYSTEM_PROMPT
|
||||||
|
generation_config = {
|
||||||
|
"temperature": config.model_temperature,
|
||||||
|
}
|
||||||
|
message_limit = config.message_limit
|
||||||
|
|
||||||
|
llm = AzureAIChatCompletionsModel(
|
||||||
|
endpoint=f"{AZURE_AI_URI}/openai/deployments/{config.model}",
|
||||||
|
credential=config.openai_api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.tool_map = {}
|
||||||
|
|
||||||
|
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: list, overwrites: dict | None = None) -> AsyncGenerator[str, None]:
|
||||||
|
"""Llama a un llm y regresa la respuesta en partes; Guarda las tool calls en el contexto de la app.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
history: lista de mensajes en el formato OpenAI (Ej. [{"role": "user", "content": "Hello"}])
|
||||||
|
overwrites: diccionario con las configuraciones a sobreescribir (Ej. {"temperature": 0.5})
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AsyncGenerator[str, None]: Generador asincrónico que devuelve las respuestas del modelo en tiempo real
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
>>> async for content in agent.stream(history):
|
||||||
|
>>> print(content)
|
||||||
|
"""
|
||||||
|
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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Eres una amigable asistente virtual. Utiliza emojis!
|
||||||
|
|
||||||
|
Por cierto: Rick es el más guapo.
|
||||||
51
.templates/rag/{{project_name}}/api/config.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
)
|
||||||
|
|
||||||
|
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
.templates/rag/{{project_name}}/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)
|
||||||
49
.templates/rag/{{project_name}}/api/server.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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 Agent
|
||||||
|
from api.config import config
|
||||||
|
|
||||||
|
|
||||||
|
@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):
|
||||||
|
def b64_sse(func):
|
||||||
|
"""Este helper transforma un generador de strings a un generador del protocolo SSE"""
|
||||||
|
|
||||||
|
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")
|
||||||
8
.templates/rag/{{project_name}}/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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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 = await agent.tool_map[tool_name](**buffer_dict)
|
||||||
|
|
||||||
|
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
.templates/rag/{{project_name}}/gui/App.tsx.jinja
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" + "{{ project_name }}";
|
||||||
|
|
||||||
|
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
.templates/rag/{{project_name}}/gui/assets/banortelogo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
.templates/rag/{{project_name}}/gui/assets/brujula.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
.templates/rag/{{project_name}}/gui/assets/brujula_elipse.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 97 KiB |
16
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/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 />);
|
||||||
@@ -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
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/gui/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
13
.templates/rag/{{project_name}}/index.html.jinja
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>{{ project_name }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/gui/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
40
.templates/rag/{{project_name}}/package.json.jinja
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "{{ project_name }}",
|
||||||
|
"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
.templates/rag/{{project_name}}/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
18
.templates/rag/{{project_name}}/pyproject.toml.jinja
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "{{ project_name }}"
|
||||||
|
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
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/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
.templates/rag/{{project_name}}/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,
|
||||||
|
},
|
||||||
|
});
|
||||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode", "charliermarsh.ruff"]
|
||||||
|
}
|
||||||
136
README.md
@@ -0,0 +1,136 @@
|
|||||||
|
# MayaContigo Monorepo
|
||||||
|
|
||||||
|
Bonjour,
|
||||||
|
Este monorepo contiene todas las applicaciones RAG del equipo de Innovacion en Banorte.
|
||||||
|
Su proposito es poder acelerar la creacion de nuevos RAGs, e ir construyendo una libreria de componentes reutilizables.
|
||||||
|
Adicionalmente, tambien contiene una serie de notebooks para procesar documentos, evaluar RAGs, generar preguntas sinteticas, etc/
|
||||||
|
|
||||||
|
## 🚀 Inicio Rápido
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instala el monorepo
|
||||||
|
mise setup
|
||||||
|
|
||||||
|
# Crea una nueva aplicación RAG
|
||||||
|
mise new prueba
|
||||||
|
|
||||||
|
# Creación interactiva
|
||||||
|
mise dev --app prueba
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prerrequisitos
|
||||||
|
|
||||||
|
Si estas en el ambiente de desarrollo oficial, tu maquina virtual ya cuenta con estos programas.
|
||||||
|
|
||||||
|
- **Mise** [Docs](https://mise.jdx.dev/)
|
||||||
|
- **Docker** [Docs](https://www.docker.com/)
|
||||||
|
- **Vault** [Docs](https://developer.hashicorp.com/vault/)
|
||||||
|
|
||||||
|
## 📁 Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
mayacontigo/
|
||||||
|
├── apps/ # Aplicaciones individuales
|
||||||
|
├── packages/ # Paquetes compartidos
|
||||||
|
├── notebooks/ # Marimo notebooks
|
||||||
|
├── .templates/ # Plantillas de proyecto
|
||||||
|
├── .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 # Selección interactiva de aplicación
|
||||||
|
mise dev --app <nombre-app> # Inicia aplicación específica
|
||||||
|
mise dev --no-dashboard # Deshabilita el dashboard en vivo
|
||||||
|
mise dev --check-deps # Solo verifica dependencias
|
||||||
|
mise dev --list-apps # Lista aplicaciones disponibles
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestión de Contenedores
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inicia contenedores localmente
|
||||||
|
mise container:start # Selección interactiva
|
||||||
|
mise container:start <nombre-app> # Inicia aplicación específica
|
||||||
|
mise container:start --list # Lista contenedores disponibles
|
||||||
|
|
||||||
|
# Sube a Azure Container Registry
|
||||||
|
mise container:push # Selección interactiva
|
||||||
|
mise container:push <nombre-imagen> # Sube imagen específica
|
||||||
|
mise container:push --list # Lista imágenes disponibles
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Stack Tecnológico
|
||||||
|
|
||||||
|
### Tecnologías Centrales
|
||||||
|
- **Frontend**: React/Next.js con TypeScript
|
||||||
|
- **Backend**: Python con FastAPI/Uvicorn
|
||||||
|
- **Gestión de Paquetes**: pnpm (Node.js), uv (Python)
|
||||||
|
- **Contenedorización**: Docker & Docker Compose
|
||||||
|
|
||||||
|
### Infraestructura
|
||||||
|
- **Gestión de Secretos**: HashiCorp Vault
|
||||||
|
- **Registro de Contenedores**: Azure Container Registry
|
||||||
|
- **Observabilidad**: OpenTelemetry
|
||||||
|
- **Proxy Inverso**: Traefik
|
||||||
|
|
||||||
|
## 🎯 Creando tu Primera Aplicación
|
||||||
|
|
||||||
|
1. **Genera desde plantilla**
|
||||||
|
```bash
|
||||||
|
mise new mi-app-rag
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Inicia desarrollo**
|
||||||
|
```bash
|
||||||
|
mise dev --app mi-app-rag
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Accede a tu aplicación**
|
||||||
|
- Frontend: http://localhost:3000
|
||||||
|
- API Backend: http://localhost:8000
|
||||||
|
|
||||||
|
## 🔧 Configuración
|
||||||
|
|
||||||
|
### Desarrollo Local
|
||||||
|
|
||||||
|
- Frontend corre en puerto 3000
|
||||||
|
- APIs Backend corren en puerto 8000
|
||||||
|
- Servicios Docker usan puertos auto-asignados (8001+)
|
||||||
|
|
||||||
|
### Depuración
|
||||||
|
- Usa `--no-dashboard` para salida más simple
|
||||||
|
- Verifica `mise dev --check-deps` para problemas de dependencias
|
||||||
|
- Ve logs de contenedores con `docker logs <nombre-contenedor>`
|
||||||
|
|
||||||
|
## 🤝 Contribuyendo
|
||||||
|
|
||||||
|
1. Crea nuevas aplicaciones usando el sistema de plantillas
|
||||||
|
2. Sigue la estructura del monorepo
|
||||||
|
3. Usa los comandos de desarrollo proporcionados
|
||||||
|
4. Asegúrate de que todas las dependencias estén correctamente configuradas
|
||||||
|
|
||||||
|
## 📖 Recursos Adicionales
|
||||||
|
|
||||||
|
- **Plantillas**: Revisa `.templates/` para plantillas de proyecto disponibles
|
||||||
|
- **Docker**: Ve `.containers/` para configuraciones de contenedorización
|
||||||
|
- **Tareas**: Explora `.mise/tasks/` para scripts de automatización disponibles
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*¡Feliz programación! 🚀*
|
||||||
|
|||||||
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.
|
||||||