File size: 3,389 Bytes
d86db02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
LangSmith observability for the MediShield classification pipeline.

Each pipeline stage is wrapped with @traceable so LangSmith records:
  - inputs / outputs per stage
  - latency per stage
  - token usage for LLM calls
  - parent/child span relationships (rules → ocr → llm under classify)

Required env vars:
  LANGCHAIN_TRACING_V2=true
  LANGCHAIN_API_KEY=<your-langsmith-key>
  LANGCHAIN_PROJECT=medishield-classification
"""

import logging
import time
from typing import Any

from langsmith import traceable

logger = logging.getLogger(__name__)


@traceable(name="rules-engine", run_type="tool", tags=["stage:rules"])
def trace_rules_engine(filename: str, doc_type: str | None, send_to_llm: bool) -> dict:
    """Record the rules engine decision in LangSmith."""
    return {
        "filename": filename,
        "doc_type": doc_type,
        "send_to_llm": send_to_llm,
        "stage": "rules",
    }


@traceable(name="kyc-ocr", run_type="tool", tags=["stage:ocr"])
def trace_kyc_ocr(filename: str, doc_type: str | None, send_to_llm: bool, ocr_text: str) -> dict:
    """Record the KYC OCR decision in LangSmith."""
    return {
        "filename": filename,
        "doc_type": doc_type,
        "send_to_llm": send_to_llm,
        "ocr_text_length": len(ocr_text),
        "stage": "ocr",
    }


@traceable(name="llm-classify", run_type="llm", tags=["stage:llm"])
def trace_llm_classify(
    filename: str,
    sub_type: str,
    input_tokens: int,
    output_tokens: int,
    raw_response: str,
) -> dict:
    """Record the LLM classification result and token usage in LangSmith."""
    return {
        "filename": filename,
        "sub_type": sub_type,
        "usage": {
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "total_tokens": input_tokens + output_tokens,
        },
        "raw_response": raw_response,
        "stage": "llm",
    }


@traceable(name="classify-document", run_type="chain", tags=["pipeline"])
def trace_classify(
    filename: str,
    doc_type: str,
    sub_type: str | None,
    method: str,
    latency_ms: int,
    input_tokens: int,
    output_tokens: int,
) -> dict:
    """Top-level trace for a single document classification run."""
    result = {
        "filename": filename,
        "doc_type": doc_type,
        "sub_type": sub_type,
        "method": method,
        "latency_ms": latency_ms,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
    }
    logger.info(
        "classified",
        extra={
            "filename": filename,
            "doc_type": doc_type,
            "sub_type": sub_type,
            "method": method,
            "latency_ms": latency_ms,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
        },
    )
    return result


def record_token_usage(response_usage_metadata: Any) -> dict[str, int]:
    """Extract token counts from a Gemini usage_metadata object."""
    if response_usage_metadata is None:
        return {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}

    input_t  = getattr(response_usage_metadata, "prompt_token_count", 0) or 0
    output_t = getattr(response_usage_metadata, "candidates_token_count", 0) or 0
    return {
        "input_tokens": input_t,
        "output_tokens": output_t,
        "total_tokens": input_t + output_t,
    }