Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/geremyjampiersalasgarcia-eng/Caso_Practico_Semillero_IA/llms.txt

Use this file to discover all available pages before exploring further.

Mesa de Ayuda IA is built around a multi-agent architecture where each domain of sales knowledge — product catalog, commercial policies, sales process, image analysis, and CRM registration — is handled by a dedicated, independently deployable LangChain agent. Rather than forcing a single generalist model to answer every question, the system routes each query to the agent (or agents) that own the relevant knowledge. LangGraph’s StateGraph was chosen as the backbone because it models the entire routing and execution flow as an explicit directed graph, providing deterministic, auditable paths with no risk of infinite loops or hallucinated tool invocations that are common in classic AgentExecutor setups. Parallel fan-out for mixed-intent queries — where three agents run simultaneously — is a first-class feature of the graph model that would require complex custom logic in a chain-based approach.

Agent Registry Pattern

All agents are managed through an AgentRegistry singleton that decouples agent discovery from the orchestrator. This means new agents can be added to the system by simply calling register() — the LangGraph graph never needs to be modified.
class AgentRegistry:
    """
    Patrón Registry para los agentes.
    Permite registrar nuevos agentes sin modificar el código del orquestador.
    """
    def __init__(self):
        self._agents: Dict[str, BaseAgent] = {}

    def register(self, agent: BaseAgent):
        """Registra una instancia de un agente"""
        if agent.name in self._agents:
            logger.warning("Sobrescribiendo agente existente", agent=agent.name)
        self._agents[agent.name] = agent
        logger.info("Agente registrado exitosamente", agent=agent.name)

    def get_agent(self, name: str) -> BaseAgent:
        """Recupera un agente por su nombre"""
        agent = self._agents.get(name)
        if not agent:
            raise AgentNotFoundError(name)
        return agent

    def list_agents(self) -> List[AgentInfo]:
        """Lista todos los agentes disponibles y sus descripciones"""
        return [
            AgentInfo(name=agent.name, description=agent.description)
            for agent in self._agents.values()
        ]

# Instancia global del registro
agent_registry = AgentRegistry()
The three key methods are:
  • register(agent) — stores any BaseAgent subclass instance under agent.name. If an agent with the same name already exists, it logs a warning and overwrites it (useful for hot-reloading during development).
  • get_agent(name) — retrieves an agent by its string identifier. Raises AgentNotFoundError if no match is found, preventing silent failures in the graph nodes.
  • list_agents() — returns a list of AgentInfo objects (name + description) for introspection, useful during development to confirm which agents are loaded and available.
The global agent_registry singleton at the bottom of registry.py is imported by every graph node in orchestrator.py. Agents are registered at startup inside backend/app/agents/__init__.py, so by the time the first HTTP request arrives, all five agents are already in the registry and ready to serve.

BaseAgent Abstract Class

Every agent in the system inherits from BaseAgent, an abstract base class that enforces a consistent interface and encapsulates the entire RAG + LLM execution pipeline. Subclasses only need to declare four properties; all the heavy lifting (retrieval, prompt construction, LLM invocation, token counting) lives in the base class. The four abstract properties that every subclass must implement are:
PropertyTypePurpose
namestrUnique identifier used as the registry key and in AgentResult.agent_name
descriptionstrHuman-readable description exposed by list_agents()
collection_namestrChromaDB collection that this agent queries (e.g. col_catalogo)
system_prompt_pathstrAbsolute path to the Markdown file containing the agent’s system prompt
The process_query() method signature and its four-step flow:
def process_query(self, question: str) -> AgentResult:
    """
    Flujo principal del agente:
    1. Busca en su colección ChromaDB
    2. Inyecta el contexto en su prompt
    3. Invoca al LLM
    4. Retorna el resultado con las fuentes
    """
    # 1. RAG Retrieval
    contexts = retrieve_context(question, self.collection_name)

    # Build context_text and sources list from retrieved chunks...

    # 2. Preparar el Prompt
    base_prompt = self._load_prompt()
    system_content = f"{base_prompt}\n\n### CONTEXTO RECUPERADO:\n{context_text}"

    messages = [
        SystemMessage(content=system_content),
        HumanMessage(content=question)
    ]

    # 3. Invocar LLM
    llm = get_llm()
    response = llm.invoke(messages)

    # 4. Retornar
    return AgentResult(
        agent_name=self.name,
        answer=str(response.content),
        sources=sources,
        tokens_input=tokens_input,
        tokens_output=tokens_output
    )
The base class also handles all error conditions gracefully: if ChromaDB is unreachable, it returns a structured AgentResult with a user-friendly error message instead of raising an uncaught exception that would crash the graph. Similarly, Gemini rate-limit errors (HTTP 429) produce a specific advisory message rather than a generic traceback.

The Five Agents

agente_catalogo

Answers questions about products, specifications, list prices, and stock availability. Queries the col_catalogo collection backed by 01_Catalogo_Productos_Precios.txt.

agente_politicas

Answers questions about discount tiers, authorization levels, credit conditions, warranties, and returns. Queries the col_politicas collection backed by 02_Politicas_Comerciales_Descuentos_Credito.txt.

agente_proceso_ventas

Covers the sales funnel stages, CRM registration requirements, and closing criteria. Queries the col_proceso_ventas collection backed by 03_Proceso_Ventas_CRM.txt.

agente_multimodal

Analyzes product images using Gemini Vision and cross-references findings against col_catalogo. Accepts a image_data parameter (base64) in addition to the text question.

agente_accion

Registers sales opportunities in data/registro_oportunidades.txt and the PostgreSQL oportunidades table using LangChain @tool Function Calling. Validates all required CRM fields, warns on discounts above 10%, and requests user confirmation before writing.
Full agent summary table:
Agent NameChromaDB CollectionPrimary Responsibility
agente_catalogocol_catalogoProducts, specs, list prices, availability
agente_politicascol_politicasDiscounts, credit, guarantees, returns
agente_proceso_ventascol_proceso_ventasSales funnel, CRM steps, closing requirements
agente_multimodalcol_catalogoImage analysis via Gemini Vision + catalog cross-reference
agente_accion(none — action agent)CRM opportunity registration via Function Calling

Intent Categories

The classifier (powered by Gemini at temperature=0.0 for maximum determinism) maps each incoming question to one of six categories defined in classifier.py:
VALID_CATEGORIES = [
    "catalogo_precios",
    "politicas_comerciales",
    "proceso_ventas",
    "accion_registro",
    "multimodal",
    "mixta",
]
CategoryMeaningRouted To
catalogo_preciosQuestions about products, specs, prices, or stockagente_catalogo
politicas_comercialesQuestions about discounts, authorization levels, credit, warranties, or returnsagente_politicas
proceso_ventasQuestions about the sales funnel, CRM steps, or closing criteriaagente_proceso_ventas
accion_registroExplicit requests to register or save a sales opportunityagente_accion
multimodalAny query that includes an attached image (detected before LLM classification)agente_multimodal
mixtaQuestions that span two or more domains simultaneouslyagente_catalogo + agente_politicas + agente_proceso_ventas (parallel)
Image detection is short-circuited before the LLM classifier runs. If has_image=True is passed, classify_intent() immediately returns category="multimodal" with confidence=1.0 — no Gemini API call is made, saving tokens and latency.
If the raw LLM response is not one of the six valid categories, the classifier attempts a partial-match fallback (e.g., if the model returns "catalogo_precios: alta confianza", it will strip and match "catalogo_precios"). If no match is found, it defaults to "mixta" to ensure the query always gets an answer.

Parallel Execution for Mixed Queries

When the classifier returns mixta, the LangGraph conditional edge fires three agent nodes simultaneously — catalogo, politicas, and proceso_ventas — rather than executing them sequentially. This is made possible by returning a list from route_agents() inside add_conditional_edges:
def route_agents(state: GraphState):
    """
    Decide qué agente(s) invocar según la categoría clasificada.
    Retorna una lista de nombres de nodo.
    """
    cat = state["category"]

    if cat == "catalogo_precios":
        return ["catalogo"]
    elif cat == "politicas_comerciales":
        return ["politicas"]
    elif cat == "proceso_ventas":
        return ["proceso_ventas"]
    elif cat == "multimodal":
        return ["multimodal"]
    elif cat == "accion_registro":
        return ["accion"]
    else:
        # mixta: invocar los 3 agentes RAG principales en paralelo
        return ["catalogo", "politicas", "proceso_ventas"]
LangGraph schedules all nodes in the returned list as parallel branches of the graph. Each branch writes its AgentResult into state["agent_results"], which is typed as Annotated[List[AgentResult], operator.add] — meaning results from parallel branches are accumulated (concatenated) rather than overwriting each other. Once all three branches complete, execution converges at the consolidate node, which calls Gemini one final time to synthesize a single coherent answer from all partial responses.
For a typical sales question involving price, discount eligibility, and CRM steps simultaneously, the mixta path fires all three RAG agents in parallel. Total wall-clock time is approximately equal to the slowest single agent call rather than three sequential calls, keeping end-to-end latency manageable even on the free-tier Gemini quota.

AgentResult Schema

Every agent node — whether a RAG agent, the multimodal agent, or the action agent — returns a standardized AgentResult Pydantic model that the consolidate node consumes:
class AgentResult(BaseModel):
    """Resultado devuelto por un agente individual tras procesar una consulta"""
    agent_name: str
    answer: str
    sources: List[SourceInfo] = []
    tokens_input: int = 0
    tokens_output: int = 0
FieldTypeDescription
agent_namestrThe agent’s registry identifier (e.g. "agente_catalogo")
answerstrThe full LLM-generated answer text for this agent’s domain
sourcesList[SourceInfo]List of ChromaDB chunks used as context, each with document name, content snippet (truncated at 200 chars), and relevance score
tokens_inputintInput tokens consumed by this agent’s Gemini call (from usage_metadata)
tokens_outputintOutput tokens produced by this agent’s Gemini call
The tokens_input and tokens_output fields flow back into GraphState via operator.add accumulation, giving the final API response an aggregate token count across all agents involved in answering a single question.

Build docs developers (and LLMs) love