PaperMate / backend /observability.py
Phuc-HugigFace's picture
@
3fa4cc8
Raw
History Blame
4.84 kB
"""Pipeline observability helpers for step, API, LLM, and cost tracking."""
from __future__ import annotations
from contextlib import asynccontextmanager
from contextvars import ContextVar
from time import perf_counter
from typing import Any, AsyncIterator
from backend.config import settings
_current_step: ContextVar[dict[str, Any] | None] = ContextVar("current_pipeline_step", default=None)
def _elapsed_ms(start: float) -> int:
return max(0, round((perf_counter() - start) * 1000))
def current_step_id() -> int | None:
"""pipeline_step id of the step currently running (None outside a step).
Lets pipeline code link evidence_items / provider_calls to the live step
without threading the id through every call.
"""
context = _current_step.get()
return context.get("step_id") if context else None
@asynccontextmanager
async def tracked_step(
access_key: str,
step_name: str,
step_order: float | None = None,
metadata: dict[str, Any] | None = None,
) -> AsyncIterator[dict[str, Any]]:
from backend.storage import jobs as job_store
started = perf_counter()
step = await job_store.start_pipeline_step(
access_key,
step_name,
step_order=step_order,
metadata=metadata,
)
token = _current_step.set(
{
"access_key": access_key,
"step_id": step.get("id"),
"step_name": step_name,
}
)
try:
yield step
except Exception as exc:
await job_store.finish_pipeline_step(
access_key,
step["id"],
"failed",
latency_ms=_elapsed_ms(started),
error=f"{type(exc).__name__}: {exc}",
)
raise
else:
# The body may stash its output on the step dict (see _run_tracked_step).
await job_store.finish_pipeline_step(
access_key,
step["id"],
"completed",
latency_ms=_elapsed_ms(started),
error=None,
output_json=step.get("_output_json"),
output_summary=step.get("_output_summary"),
)
finally:
_current_step.reset(token)
async def record_llm_call(
provider: str,
model: str,
response_format: str | None,
prompt_tokens: int | None,
completion_tokens: int | None,
total_tokens: int | None,
latency_ms: int | None,
status: str,
error: str | None,
) -> None:
context = _current_step.get()
if context is None:
return
from backend.storage import jobs as job_store
await job_store.record_llm_call(
access_key=context["access_key"],
step_id=context.get("step_id"),
step_name=context.get("step_name"),
provider=provider,
model=model,
response_format=response_format,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
input_price_per_1m=_input_price_for_provider(provider),
output_price_per_1m=_output_price_for_provider(provider),
latency_ms=latency_ms,
status=status,
error=error,
)
async def record_api_call(
provider: str,
operation: str,
status_code: int | None = None,
latency_ms: int | None = None,
request_count: int = 1,
status: str = "success",
error: str | None = None,
cost_usd: float | None = None,
metadata: dict[str, Any] | None = None,
) -> None:
context = _current_step.get()
if context is None:
return
from backend.storage import jobs as job_store
await job_store.record_api_call(
access_key=context["access_key"],
step_id=context.get("step_id"),
step_name=context.get("step_name"),
provider=provider,
operation=operation,
status_code=status_code,
latency_ms=latency_ms,
request_count=request_count,
status=status,
error=error,
cost_usd=cost_usd,
metadata=metadata,
)
def _input_price_for_provider(provider: str) -> float | None:
if provider == "openai":
return settings.openai_input_price_per_1m
if provider == "anthropic":
return settings.anthropic_input_price_per_1m
if provider == "gemini":
return settings.gemini_input_price_per_1m
if provider == "openrouter":
return settings.openrouter_input_price_per_1m
return None
def _output_price_for_provider(provider: str) -> float | None:
if provider == "openai":
return settings.openai_output_price_per_1m
if provider == "anthropic":
return settings.anthropic_output_price_per_1m
if provider == "gemini":
return settings.gemini_output_price_per_1m
if provider == "openrouter":
return settings.openrouter_output_price_per_1m
return None