Jiya3177 commited on
Commit
a1b3bc8
·
1 Parent(s): aaff8ef

feat: add optional langsmith tracing

Browse files
.env.example CHANGED
@@ -81,6 +81,24 @@ HF_TOKEN=your_huggingface_token_here
81
  # Optional — defaults to 1024
82
  # LLM_MAX_NEW_TOKENS=1024
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  # ── Embeddings (Optional — defaults shown)──────────────────────────────────────────────
85
 
86
  # SentenceTransformer model ID for generating document embeddings.
 
81
  # Optional — defaults to 1024
82
  # LLM_MAX_NEW_TOKENS=1024
83
 
84
+ # ── LangSmith Tracing (Optional) ────────────────────────
85
+
86
+ # Enable LangSmith tracing for the backend RAG pipeline.
87
+ # Optional — defaults to False
88
+ # LANGSMITH_TRACING=False
89
+
90
+ # LangSmith API key.
91
+ # Optional — only needed when LANGSMITH_TRACING=True
92
+ # LANGSMITH_API_KEY=
93
+
94
+ # LangSmith API endpoint.
95
+ # Optional — defaults to "https://api.smith.langchain.com"
96
+ # LANGSMITH_ENDPOINT=https://api.smith.langchain.com
97
+
98
+ # LangSmith project name used for traced runs.
99
+ # Optional — defaults to "pdf-assistant-rag"
100
+ # LANGSMITH_PROJECT=pdf-assistant-rag
101
+
102
  # ── Embeddings (Optional — defaults shown)──────────────────────────────────────────────
103
 
104
  # SentenceTransformer model ID for generating document embeddings.
backend/app/config.py CHANGED
@@ -54,6 +54,12 @@ class Settings(BaseSettings):
54
  LLM_MAX_NEW_TOKENS: int = 1024
55
  LLM_TEMPERATURE: float = 0.3
56
 
 
 
 
 
 
 
57
  # ── Reranker ─────────────────────────────────────────
58
  RERANKER_MODEL: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"
59
 
 
54
  LLM_MAX_NEW_TOKENS: int = 1024
55
  LLM_TEMPERATURE: float = 0.3
56
 
57
+ # ── LangSmith Tracing (optional) ─────────────────────
58
+ LANGSMITH_TRACING: bool = False
59
+ LANGSMITH_API_KEY: str = ""
60
+ LANGSMITH_ENDPOINT: str = "https://api.smith.langchain.com"
61
+ LANGSMITH_PROJECT: str = "pdf-assistant-rag"
62
+
63
  # ── Reranker ─────────────────────────────────────────
64
  RERANKER_MODEL: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"
65
 
backend/app/rag/agent.py CHANGED
@@ -10,6 +10,7 @@ from huggingface_hub import InferenceClient
10
  from app.config import get_settings
11
  from app.rag.retriever import retrieve
12
  from app.rag.prompts import SYSTEM_PROMPT, RAG_PROMPT_TEMPLATE, GREETING_PROMPT
 
13
 
14
  logger = logging.getLogger(__name__)
15
  settings = get_settings()
@@ -65,6 +66,14 @@ def _chat_messages(system: str, user_content: str) -> list:
65
  ]
66
 
67
 
 
 
 
 
 
 
 
 
68
  def generate_answer(
69
  question: str,
70
  user_id: str,
@@ -145,6 +154,14 @@ def generate_answer(
145
  return {"answer": answer, "sources": sources}
146
 
147
 
 
 
 
 
 
 
 
 
148
  def generate_answer_stream(
149
  question: str,
150
  user_id: str,
 
10
  from app.config import get_settings
11
  from app.rag.retriever import retrieve
12
  from app.rag.prompts import SYSTEM_PROMPT, RAG_PROMPT_TEMPLATE, GREETING_PROMPT
13
+ from app.rag.tracing import trace_function
14
 
15
  logger = logging.getLogger(__name__)
16
  settings = get_settings()
 
66
  ]
67
 
68
 
69
+ @trace_function(
70
+ "generate_answer",
71
+ metadata_factory=lambda question, user_id, document_id=None: {
72
+ "user_id": user_id,
73
+ "document_id": document_id,
74
+ "llm_model": settings.LLM_MODEL,
75
+ },
76
+ )
77
  def generate_answer(
78
  question: str,
79
  user_id: str,
 
154
  return {"answer": answer, "sources": sources}
155
 
156
 
157
+ @trace_function(
158
+ "generate_answer_stream",
159
+ metadata_factory=lambda question, user_id, document_id=None: {
160
+ "user_id": user_id,
161
+ "document_id": document_id,
162
+ "llm_model": settings.LLM_MODEL,
163
+ },
164
+ )
165
  def generate_answer_stream(
166
  question: str,
167
  user_id: str,
backend/app/rag/embeddings.py CHANGED
@@ -6,6 +6,7 @@ import logging
6
  from typing import List
7
  from langchain_huggingface import HuggingFaceEmbeddings
8
  from app.config import get_settings
 
9
 
10
  logger = logging.getLogger(__name__)
11
  settings = get_settings()
@@ -36,10 +37,26 @@ def get_embedding_model() -> HuggingFaceEmbeddings:
36
  def embed_texts(texts: List[str]) -> List[List[float]]:
37
  """Embed a batch of texts into vectors."""
38
  model = get_embedding_model()
39
- return model.embed_documents(texts)
 
 
 
 
 
 
 
 
40
 
41
 
42
  def embed_query(query: str) -> List[float]:
43
  """Embed a single query string."""
44
  model = get_embedding_model()
45
- return model.embed_query(query)
 
 
 
 
 
 
 
 
 
6
  from typing import List
7
  from langchain_huggingface import HuggingFaceEmbeddings
8
  from app.config import get_settings
9
+ from app.rag.tracing import trace_call
10
 
11
  logger = logging.getLogger(__name__)
12
  settings = get_settings()
 
37
  def embed_texts(texts: List[str]) -> List[List[float]]:
38
  """Embed a batch of texts into vectors."""
39
  model = get_embedding_model()
40
+ return trace_call(
41
+ "embed_texts",
42
+ lambda: model.embed_documents(texts),
43
+ run_type="embedding",
44
+ metadata={
45
+ "embedding_model": settings.EMBEDDING_MODEL,
46
+ "text_count": len(texts),
47
+ },
48
+ )
49
 
50
 
51
  def embed_query(query: str) -> List[float]:
52
  """Embed a single query string."""
53
  model = get_embedding_model()
54
+ return trace_call(
55
+ "embed_query",
56
+ lambda: model.embed_query(query),
57
+ run_type="embedding",
58
+ metadata={
59
+ "embedding_model": settings.EMBEDDING_MODEL,
60
+ "query_length": len(query),
61
+ },
62
+ )
backend/app/rag/retriever.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
  from typing import List, Dict, Any, Optional
6
  from app.config import get_settings
7
  from app.rag.embeddings import embed_query
 
8
  from app.rag.vectorstore import query_chunks
9
 
10
  logger = logging.getLogger(__name__)
@@ -31,6 +32,17 @@ def get_reranker():
31
  return _reranker if _reranker != "disabled" else None
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
34
  def retrieve(
35
  query: str,
36
  user_id: str,
 
5
  from typing import List, Dict, Any, Optional
6
  from app.config import get_settings
7
  from app.rag.embeddings import embed_query
8
+ from app.rag.tracing import trace_function
9
  from app.rag.vectorstore import query_chunks
10
 
11
  logger = logging.getLogger(__name__)
 
32
  return _reranker if _reranker != "disabled" else None
33
 
34
 
35
+ @trace_function(
36
+ "retrieve",
37
+ metadata_factory=lambda query, user_id, document_id=None: {
38
+ "user_id": user_id,
39
+ "document_id": document_id,
40
+ "embedding_model": settings.EMBEDDING_MODEL,
41
+ "reranker_model": settings.RERANKER_MODEL,
42
+ "top_k_retrieval": settings.TOP_K_RETRIEVAL,
43
+ "top_k_rerank": settings.TOP_K_RERANK,
44
+ },
45
+ )
46
  def retrieve(
47
  query: str,
48
  user_id: str,
backend/app/rag/tracing.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Optional LangSmith tracing helpers for the RAG pipeline.
3
+ Safe to import even when LangSmith is not installed or configured.
4
+ """
5
+ import logging
6
+ import os
7
+ from functools import wraps
8
+ from typing import Any, Callable, Optional
9
+
10
+ from app.config import get_settings
11
+
12
+ logger = logging.getLogger(__name__)
13
+ settings = get_settings()
14
+
15
+ try:
16
+ from langsmith import traceable as _langsmith_traceable
17
+ except Exception: # pragma: no cover - optional dependency safety
18
+ _langsmith_traceable = None
19
+
20
+
21
+ def configure_langsmith() -> bool:
22
+ """Configure LangSmith environment variables when tracing is enabled."""
23
+ if not settings.LANGSMITH_TRACING:
24
+ return False
25
+
26
+ if not settings.LANGSMITH_API_KEY:
27
+ logger.warning("LangSmith tracing enabled but LANGSMITH_API_KEY is not set; tracing disabled.")
28
+ return False
29
+
30
+ os.environ["LANGSMITH_TRACING"] = "true"
31
+ os.environ["LANGSMITH_API_KEY"] = settings.LANGSMITH_API_KEY
32
+ os.environ["LANGSMITH_ENDPOINT"] = settings.LANGSMITH_ENDPOINT
33
+ os.environ["LANGSMITH_PROJECT"] = settings.LANGSMITH_PROJECT
34
+ return _langsmith_traceable is not None
35
+
36
+
37
+ LANGSMITH_ENABLED = configure_langsmith()
38
+
39
+
40
+ def _sanitize_metadata(metadata: Optional[dict[str, Any]]) -> dict[str, Any]:
41
+ return {key: value for key, value in (metadata or {}).items() if value is not None}
42
+
43
+
44
+ def _build_traceable(name: str, run_type: str, metadata: Optional[dict[str, Any]] = None):
45
+ """Build a LangSmith traceable decorator safely across versions."""
46
+ if _langsmith_traceable is None:
47
+ return None
48
+
49
+ sanitized = _sanitize_metadata(metadata)
50
+ try:
51
+ return _langsmith_traceable(
52
+ name=name,
53
+ run_type=run_type,
54
+ metadata=sanitized or None,
55
+ )
56
+ except TypeError:
57
+ return _langsmith_traceable(name=name, run_type=run_type)
58
+
59
+
60
+ def trace_call(
61
+ name: str,
62
+ fn: Callable[..., Any],
63
+ *args: Any,
64
+ run_type: str = "chain",
65
+ metadata: Optional[dict[str, Any]] = None,
66
+ **kwargs: Any,
67
+ ) -> Any:
68
+ """Execute a callable with LangSmith tracing when available."""
69
+ if not LANGSMITH_ENABLED:
70
+ return fn(*args, **kwargs)
71
+
72
+ decorator = _build_traceable(name, run_type, metadata)
73
+ if decorator is None:
74
+ return fn(*args, **kwargs)
75
+
76
+ traced_fn = decorator(fn)
77
+ return traced_fn(*args, **kwargs)
78
+
79
+
80
+ def trace_function(
81
+ name: str,
82
+ *,
83
+ run_type: str = "chain",
84
+ metadata_factory: Optional[Callable[..., dict[str, Any]]] = None,
85
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
86
+ """Decorator wrapper that becomes a no-op when LangSmith is disabled."""
87
+ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
88
+ @wraps(fn)
89
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
90
+ metadata = metadata_factory(*args, **kwargs) if metadata_factory else None
91
+ return trace_call(
92
+ name,
93
+ fn,
94
+ *args,
95
+ run_type=run_type,
96
+ metadata=metadata,
97
+ **kwargs,
98
+ )
99
+
100
+ return wrapped
101
+
102
+ return decorator
backend/requirements.txt CHANGED
@@ -27,6 +27,7 @@ langchain
27
  langchain-community
28
  langchain-huggingface
29
  langchain-text-splitters
 
30
 
31
  # Embeddings & ML
32
  sentence-transformers
 
27
  langchain-community
28
  langchain-huggingface
29
  langchain-text-splitters
30
+ langsmith
31
 
32
  # Embeddings & ML
33
  sentence-transformers