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 implements the O (Observability) and C (Costs) pillars of the E-O-C-S framework. Every request processed by the LangGraph orchestrator is exported as an OpenTelemetry trace to Arize Phoenix, giving operators full visibility into which agent nodes ran, how many tokens each Gemini call consumed, and how much that call cost in USD. This page explains how the instrumentation is wired together and how to use the resulting data.

Arize Phoenix Setup

Phoenix runs as a Docker container alongside PostgreSQL. The docker-compose.yml exposes it on port 6006 (UI + OTLP HTTP) and maps OTLP gRPC on port 4317:
phoenix:
  image: arizephoenix/phoenix:latest
  container_name: mesa_ayuda_phoenix
  ports:
    - "6006:6006"
    - "4317:4317"
    - "4318:4318"
  environment:
    - PHOENIX_HOST=0.0.0.0
    - PHOENIX_PORT=6006
Inside the FastAPI lifespan startup hook (app/main.py), the backend configures an OTLPSpanExporter pointed at http://localhost:6006/v1/traces, creates a TracerProvider with a BatchSpanProcessor, and then activates the LangChainInstrumentor to auto-instrument every LangChain and LangGraph call:
os.environ["PHOENIX_PROJECT_NAME"] = "Patito_SA_Ventas"

resource = Resource(attributes={
    "project.name": "Patito_SA_Ventas",
    "openinference.project.name": "Patito_SA_Ventas",
    "service.name": "Patito_SA_Ventas",
})

exporter = OTLPSpanExporter(
    endpoint="http://localhost:6006/v1/traces",
    headers={"project-name": "Patito_SA_Ventas"},
)

tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)

LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
The project name is set via the PHOENIX_PROJECT_NAME environment variable and appears as Patito_SA_Ventas in the Phoenix UI. The instrumentation is tolerant to failures — if the Phoenix container is not reachable, the backend logs a warning and continues without tracing.
Phoenix must be started before the backend for the OTLPSpanExporter connection to succeed during lifespan startup. Always run docker-compose up -d postgres phoenix first, wait for the containers to report as healthy, and only then start uvicorn.

What Is Traced

Because LangChainInstrumentor auto-instruments the entire LangChain/LangGraph stack, every node of the StateGraph is captured as its own OpenTelemetry span without any manual instrumentation in agent code. The graph has the following key nodes:
NodeDescription
classifyGemini call at temperature=0 that detects intent category
agente_catalogoRAG agent for product catalog and pricing
agente_politicasRAG agent for commercial policies
agente_proceso_ventasRAG agent for the sales process and CRM
agente_multimodalGemini Vision agent for image analysis
agente_accionFunction-calling agent for CRM registration
consolidateFinal Gemini call that merges multi-agent answers
Each span carries:
  • Node name — which graph node or chain produced the span
  • Input / output — the prompt sent to Gemini and the raw text response
  • Token countsprompt_token_count (input) and candidates_token_count (output) from Gemini’s usage_metadata
  • Latency — wall-clock duration of the LLM call in milliseconds
For mixed-intent queries (mixta), LangGraph fires three agent nodes in sequence, so you will see a parent trace with multiple child spans — one per agent plus the final consolidator.

Viewing Traces

Once the backend has handled at least one chat request, open http://localhost:6006 in your browser. Phoenix presents:
  • Traces list — one trace per POST /api/v1/chat request, labeled with the service.name from the resource (Patito_SA_Ventas)
  • Span waterfall — a hierarchical tree showing the classify span, each agent span, and the consolidate span, with start times and durations
  • Token consumption — per-span breakdown of input and output tokens, visible in the span detail panel
  • Latency per agent — the waterfall makes it easy to identify which agent is slowest
Send three or four different types of chat messages (a catalog query, a policies question, and a mixed question) before opening Phoenix. This populates multiple trace types and makes it easy to compare token costs across intent categories in the span list.

Cost Metrics

In addition to Phoenix traces, every request is recorded in the audit_logs PostgreSQL table by ChatService. The full set of columns defined in app/models/audit_log.py is:
ColumnTypeDescription
idStringUUID primary key, auto-generated
intent_categoryStringClassified intent (catalogo_precios, politicas_comerciales, etc.)
agents_invokedJSONList of agent node names that ran for this request
sources_retrievedJSONList of RAG source snippets returned by the agents
latency_msFloatTotal request latency in milliseconds
tokens_usedIntegerCombined total token count (input + output)
conversation_idStringID of the conversation this request belongs to
trace_idStringOpenTelemetry trace ID linking back to the Phoenix span
tokens_inputIntegerPrompt token count from Gemini usage_metadata
tokens_outputIntegerCompletion token count from Gemini usage_metadata
cost_usdFloatComputed cost: (tokens_input / 1_000_000 × $0.075) + (tokens_output / 1_000_000 × $0.30)
cost_usd is computed directly in ChatService.process_chat() using Gemini Flash pricing rates:
cost_usd = (tokens_input / 1_000_000 * 0.075) + (tokens_output / 1_000_000 * 0.30)
To query aggregated costs broken down by intent category, use the GET /api/v1/metrics/costs endpoint:
# Costs for the last 7 days (default)
curl http://localhost:8000/api/v1/metrics/costs?days=7

# Costs for the last 30 days
curl http://localhost:8000/api/v1/metrics/costs?days=30
Example response:
{
  "period_days": 7,
  "total_cost_usd": 0.00312,
  "by_intent": [
    { "intent": "catalogo_precios", "cost_usd": 0.00104, "requests": 12 },
    { "intent": "mixta",            "cost_usd": 0.00148, "requests": 6  },
    { "intent": "politicas_comerciales", "cost_usd": 0.00060, "requests": 8 }
  ]
}
The endpoint reads directly from audit_logs using a SQLAlchemy aggregate query grouped by intent_category, so it reflects every conversation processed since the backend started writing audit records.

Structured Logging

All backend components write structured JSON logs via structlog. The logger is initialised in app/utils/logger.py and configured with the following processors:
  • add_log_level — adds the severity level to every log entry
  • add_logger_name — includes the Python module name
  • TimeStamper(fmt="iso") — ISO 8601 timestamps
  • StackInfoRenderer — attaches stack-frame information when present
  • format_exc_info — formats exception tracebacks into the log record
  • JSONRenderer — outputs each log line as a single-line JSON object for easy ingestion into log aggregators
The log verbosity is controlled by the LOG_LEVEL environment variable (default: INFO). Set it to DEBUG in .env to see prompt content and retriever results:
LOG_LEVEL=DEBUG
Key fields present in agent-level log entries:
{
  "event": "Invocando orquestador",
  "conversation_id": "3a7f…",
  "level": "info",
  "logger": "app.services.chat_service",
  "timestamp": "2025-01-15T10:23:44.812Z"
}

Build docs developers (and LLMs) love