File size: 6,186 Bytes
2e3520f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55d52e5
57e7291
55d52e5
 
57e7291
55d52e5
2e3520f
 
 
 
 
57e7291
 
2e3520f
 
 
 
 
 
 
 
 
57e7291
2e3520f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57e7291
 
2e3520f
 
 
 
 
 
 
 
 
 
 
 
57e7291
2e3520f
 
 
 
 
 
 
 
 
 
57e7291
 
 
2e3520f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57e7291
2e3520f
 
57e7291
2e3520f
57e7291
 
2e3520f
 
 
55d52e5
 
57e7291
55d52e5
57e7291
 
 
2e3520f
 
57e7291
 
 
2e3520f
 
 
57e7291
2e3520f
55d52e5
2e3520f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
"""
Gradio app v2 que expone las 4 tools como MCP Server (streamable HTTP).

Diferencia con v1: la base ChromaDB se descarga del dataset HF Hub al primer
uso. La carga del modelo de embeddings + descarga del snapshot se hace lazy
en la primera tool call, no al arrancar el Space.

Local:
    uv run python -m rag_books_mcp.app

HF Spaces:
    Ver `deploy_to_hf_space.py`.
"""

from __future__ import annotations

import gradio as gr

from rag_books_mcp.tools import (
    cite_foundation,
    get_section,
    list_available_topics,
    search_theory,
)


def _build_search_tab() -> gr.Interface:
    return gr.Interface(
        fn=search_theory,
        inputs=[
            gr.Textbox(
                label="query",
                value="bias-variance tradeoff",
                placeholder="Consulta en lenguaje natural",
            ),
            gr.Radio(
                choices=["all", "both", "esl", "islp", "fes", "pdsh", "r4ds"],
                value="all",
                label="book",
                info="R4DS está en R/tidyverse; los principios se traducen a pandas/seaborn.",
            ),
            gr.Slider(minimum=1, maximum=10, step=1, value=5, label="top_k"),
        ],
        outputs=gr.Markdown(label="Resultados"),
        title="🔎 search_theory",
        description=(
            "Búsqueda semántica en ESL, ISLP, FES, PDSH y R4DS. Devuelve los "
            "fragmentos más relevantes ordenados por similitud."
        ),
        api_name="search_theory",
    )


def _build_get_section_tab() -> gr.Interface:
    return gr.Interface(
        fn=get_section,
        inputs=[
            gr.Radio(choices=["esl", "islp", "fes", "pdsh", "r4ds"], value="islp", label="book"),
            gr.Textbox(
                label="chapter",
                value="8 Tree-Based Methods",
                placeholder="Nombre del capítulo (búsqueda parcial soportada)",
            ),
            gr.Textbox(
                label="section",
                value="",
                placeholder="(Opcional) Nombre de la sección",
            ),
            gr.Slider(minimum=1, maximum=15, step=1, value=5, label="max_chunks"),
        ],
        outputs=gr.Markdown(label="Sección"),
        title="📑 get_section",
        description=(
            "Recupera una sección específica de ESL, ISLP, FES, PDSH o R4DS. Si "
            "no se encuentra por metadata, hace fallback a búsqueda semántica."
        ),
        api_name="get_section",
    )


def _build_cite_tab() -> gr.Interface:
    return gr.Interface(
        fn=cite_foundation,
        inputs=[
            gr.Textbox(
                label="topic",
                value="ridge regression",
                placeholder="Tema a fundamentar (ej: 'bagging', 'feature selection', 'EDA')",
            ),
            gr.Radio(
                choices=["brief", "medium", "deep"],
                value="medium",
                label="detail_level",
            ),
        ],
        outputs=gr.Markdown(label="Fundamentación"),
        title="📚 cite_foundation",
        description=(
            "Fundamentación teórica que cita los 5 libros: ISLP (intuitivo), "
            "ESL (riguroso), FES (feature engineering), PDSH (código Python) y "
            "R4DS (workflow iterativo de EDA y data wrangling)."
        ),
        api_name="cite_foundation",
    )


def _build_list_topics_tab() -> gr.Interface:
    return gr.Interface(
        fn=list_available_topics,
        inputs=[],
        outputs=gr.Markdown(label="Contenido indexado"),
        title="🗂️ list_available_topics",
        description="Lista los capítulos y secciones indexados en ChromaDB.",
        api_name="list_available_topics",
    )


def build_demo() -> gr.Blocks:
    """Construye la UI tabulada del MCP Server v2."""
    with gr.Blocks(title="rag-books-mcp v2 · ESL + ISLP + FES + PDSH + R4DS") as demo:
        gr.Markdown(
            """
            # 📖 RAG Books MCP v2 — ESL + ISLP + FES + PDSH + R4DS

            Servidor MCP que expone búsqueda semántica sobre cinco libros de
            referencia de Statistical Learning, Data Science y Data Wrangling:

            - **ESL** — *The Elements of Statistical Learning* (Hastie, Tibshirani, Friedman)
            - **ISLP** — *An Introduction to Statistical Learning with Python* (James, Witten, Hastie, Tibshirani)
            - **FES** — *Feature Engineering and Selection* (Kuhn, Johnson)
            - **PDSH** — *Python Data Science Handbook* (VanderPlas)
            - **R4DS** — *R for Data Science, 2nd Ed.* (Wickham, Çetinkaya-Rundel, Grolemund) — _ejemplos en R/tidyverse, principios universales para EDA y data wrangling_

            > ℹ️ R4DS está bajo licencia CC BY-NC-ND 3.0 US y está incluido en
            > este dataset bajo uso académico. Detalles, atribución y mecanismo
            > de takedown en el [DATA_CARD del dataset](https://huggingface.co/datasets/gusdelact/rag-esl-islp-chromadb).

            **v2 vs v1:** la base ChromaDB se carga desde el dataset HF
            `gusdelact/rag-esl-islp-chromadb` (tag `v2.2.0`) en lugar de
            empaquetarla con el código. Permite versionar el índice
            independientemente y reusarlo desde otros clientes.

            **Endpoint MCP:** `/gradio_api/mcp/` (streamable HTTP).
            **Embeddings:** `sentence-transformers/all-MiniLM-L6-v2` (local, sin API key).
            **Vector store:** ChromaDB con 3689 chunks (1093 ESL + 884 ISLP + 465 FES + 563 PDSH + 684 R4DS).

            La primera tool call descarga el dataset (~95 MB). Las siguientes
            son cache hit.
            """
        )

        gr.TabbedInterface(
            interface_list=[
                _build_search_tab(),
                _build_cite_tab(),
                _build_get_section_tab(),
                _build_list_topics_tab(),
            ],
            tab_names=["search_theory", "cite_foundation", "get_section", "list_available_topics"],
        )

    return demo


def main() -> None:
    demo = build_demo()
    demo.launch(mcp_server=True, server_name="0.0.0.0")


if __name__ == "__main__":
    main()