Skip to content

Commit 4ea3e96

Browse files
committed
refactor: improve logging format and exception handling across multiple modules
1 parent 23cafaa commit 4ea3e96

12 files changed

Lines changed: 115 additions & 94 deletions

File tree

libs/admin-api-lib/src/admin_api_lib/impl/file_services/s3_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def delete_file(self, file_name: str) -> None:
125125
try:
126126
file_name = f"/{file_name}" if not file_name.startswith("/") else file_name
127127
self._s3_client.delete_object(Bucket=self._s3_settings.bucket, Key=file_name)
128-
logger.info(f"File {file_name} successfully deleted.")
129-
except Exception as e:
130-
logger.error("Error deleting file %s: %s %s" % (file_name, e, traceback.format_exc()))
128+
logger.info("File %s successfully deleted.", file_name)
129+
except Exception:
130+
logger.exception("Error deleting file %s", file_name)
131131
raise

libs/extractor-api-lib/src/extractor_api_lib/impl/extractors/file_extractors/pdf_extractor.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ async def aextract_content(self, file_path: Path, name: str) -> list[InternalInf
154154
)
155155
pdf_elements += new_pdf_elements
156156

157-
logger.info(f"Extraction completed. Found {len(pdf_elements)} information pieces.")
158-
return pdf_elements
157+
158+
logger.info("Extraction completed. Found %d information pieces.", len(pdf_elements))
159+
return pdf_elements
159160

160161
def _is_text_based(self, page: Page) -> bool:
161162
"""Classify whether a page is text-based, scanned.
@@ -200,8 +201,8 @@ def _extract_tables_from_text_page(
200201
table_df = pd.DataFrame(table_data)
201202
try:
202203
converted_table = self._dataframe_converter.convert(table_df)
203-
except TypeError as e:
204-
logger.error(f"Error while converting table to string: {e}")
204+
except TypeError:
205+
logger.exception("Error while converting table to string")
205206
continue
206207
if not converted_table.strip():
207208
continue
@@ -215,8 +216,8 @@ def _extract_tables_from_text_page(
215216
information_id=hash_datetime(),
216217
)
217218
)
218-
except Exception as e:
219-
logger.warning(f"Failed to find tables on page {page_index}: {e}")
219+
except Exception:
220+
logger.warning("Failed to find tables on page %d", page_index, exc_info=True)
220221

221222
return table_elements
222223

@@ -321,19 +322,19 @@ def _extract_tables_from_scanned_page(
321322
},
322323
)
323324
)
324-
except Exception as e:
325-
logger.warning(f"Failed to convert Camelot table {i + 1}: {e}")
325+
except Exception:
326+
logger.warning("Failed to convert Camelot table %d", i + 1, exc_info=True)
326327

327-
except Exception as e:
328-
logger.debug(f"Camelot table extraction failed for page {page_index}: {e}")
328+
except Exception:
329+
logger.debug("Camelot table extraction failed for page %d", page_index, exc_info=True)
329330

330331
return table_elements
331332

332333
def _extract_text_from_text_page(self, page: Page) -> str:
333334
try:
334335
return page.extract_text() or ""
335-
except Exception as e:
336-
logger.warning(f"Failed to extract text with pdfplumber: {e}")
336+
except Exception:
337+
logger.warning("Failed to extract text with pdfplumber", exc_info=True)
337338
return ""
338339

339340
def _extract_content_from_page(

libs/extractor-api-lib/tests/pdf_extractor_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,8 +551,8 @@ async def test_end_to_end_extraction(self, pdf_extractor, test_pdf_files):
551551
text_count = sum(1 for elem in result if elem.type == ContentType.TEXT)
552552
table_count = sum(1 for elem in result if elem.type == ContentType.TABLE)
553553

554-
logger.info(f" Text elements: {text_count}")
555-
logger.info(f" Table elements: {table_count}")
554+
logger.info(" Text elements: %d", text_count)
555+
logger.info(" Table elements: %d", table_count)
556556

557557
# Verify metadata completeness
558558
for i, element in enumerate(result):

libs/rag-core-api/src/rag_core_api/impl/answer_generation_chains/language_detection_chain.py

Lines changed: 60 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,12 @@
1313

1414
from langchain_core.output_parsers import StrOutputParser
1515
from langchain_core.runnables import Runnable, RunnableConfig
16+
from pydantic import BaseModel, field_validator
17+
from langchain_core.output_parsers import PydanticOutputParser
1618

1719
from rag_core_api.impl.graph.graph_state.graph_state import AnswerGraphState
1820
from rag_core_lib.runnables.async_runnable import AsyncRunnable
1921
from rag_core_lib.impl.langfuse_manager.langfuse_manager import LangfuseManager
20-
from pydantic import BaseModel, field_validator
21-
from langchain_core.output_parsers import PydanticOutputParser
22-
23-
24-
RunnableInput = AnswerGraphState
25-
RunnableOutput = str
26-
27-
2822
from rag_core_api.utils.utils import (
2923
strip_code_fences,
3024
norm_lang,
@@ -35,13 +29,69 @@
3529
)
3630

3731

32+
RunnableInput = AnswerGraphState
33+
RunnableOutput = str
34+
35+
3836
class LanguageDetectionChain(AsyncRunnable[RunnableInput, RunnableOutput]):
3937
"""Base class for language detection of the input question."""
4038

4139
def __init__(self, langfuse_manager: LangfuseManager):
4240
"""Initialize LanguageDetectionChain with LangfuseManager."""
4341
self._langfuse_manager = langfuse_manager
4442

43+
@staticmethod
44+
def _strict_json_parse(text: str) -> Optional[str]:
45+
"""Attempt to strictly parse JSON and extract the language code."""
46+
try:
47+
data = json.loads(text)
48+
c = extract_lang_from_parsed(data)
49+
if c:
50+
return c
51+
except (json.JSONDecodeError, TypeError, ValueError):
52+
return None
53+
54+
@staticmethod
55+
def _loose_json_parse(text: str) -> Optional[str]:
56+
for pattern in (JSONISH_DQ_RE, JSONISH_SQ_RE, JSONISH_LOOSE_RE):
57+
m = pattern.search(text)
58+
if m:
59+
c = norm_lang(m.group(1))
60+
if c:
61+
return c
62+
return None
63+
64+
@staticmethod
65+
def _extract_language_code(raw_output: Any) -> str:
66+
"""Extract a two-letter ISO 639-1 language code; default to 'en' on failure."""
67+
# 1) Already parsed structures
68+
c = extract_lang_from_parsed(raw_output)
69+
if c:
70+
return c
71+
72+
# 2) Strings (common case)
73+
if isinstance(raw_output, str):
74+
text = raw_output.strip().lstrip("\ufeff")
75+
text = strip_code_fences(text)
76+
77+
# a) Direct 'xx' or 'xx-YY'
78+
c = norm_lang(text)
79+
if c:
80+
return c
81+
82+
# b) Try strict JSON parsing
83+
c = LanguageDetectionChain._strict_json_parse(text)
84+
if c:
85+
return c
86+
87+
# c) JSON-ish fallbacks (handles single quotes, loose key/value, etc.)
88+
c = LanguageDetectionChain._loose_json_parse(text)
89+
if c:
90+
return c
91+
92+
# Fallback
93+
return "en"
94+
4595
async def ainvoke(
4696
self, chain_input: RunnableInput, config: Optional[RunnableConfig] = None, **kwargs: Any
4797
) -> RunnableOutput:
@@ -73,56 +123,15 @@ def two_letter_lower(cls, v: str) -> str:
73123
# when the model doesn't follow instructions. Instead, we keep a string parser and
74124
# parse defensively in ainvoke(). We still inject format instructions to guide the LLM.
75125
try:
76-
fmt_instructions = PydanticOutputParser(
77-
pydantic_object=_LangSchema
78-
).get_format_instructions()
126+
fmt_instructions = PydanticOutputParser(pydantic_object=_LangSchema).get_format_instructions()
79127
except Exception:
80128
fmt_instructions = (
81129
"Return a strict JSON object with a single field 'language' as a "
82-
"lowercase two-letter ISO 639-1 code, e.g. {\"language\":\"de\"}."
130+
'lowercase two-letter ISO 639-1 code, e.g. {"language":"de"}.'
83131
)
84132

85133
prompt = self._langfuse_manager.get_base_prompt(self.__class__.__name__).partial(
86134
format_instructions=fmt_instructions
87135
)
88136

89137
return prompt | self._langfuse_manager.get_base_llm(self.__class__.__name__) | StrOutputParser()
90-
91-
@staticmethod
92-
def _extract_language_code(raw_output: Any) -> str:
93-
"""Extract a two-letter ISO 639-1 language code; default to 'en' on failure."""
94-
95-
# 1) Already parsed structures
96-
c = extract_lang_from_parsed(raw_output)
97-
if c:
98-
return c
99-
100-
# 2) Strings (common case)
101-
if isinstance(raw_output, str):
102-
text = raw_output.strip().lstrip("\ufeff")
103-
text = strip_code_fences(text)
104-
105-
# a) Direct 'xx' or 'xx-YY'
106-
c = norm_lang(text)
107-
if c:
108-
return c
109-
110-
# b) Try strict JSON parsing
111-
try:
112-
data = json.loads(text)
113-
c = extract_lang_from_parsed(data)
114-
if c:
115-
return c
116-
except Exception:
117-
pass
118-
119-
# c) JSON-ish fallbacks (handles single quotes, loose key/value, etc.)
120-
for pattern in (JSONISH_DQ_RE, JSONISH_SQ_RE, JSONISH_LOOSE_RE):
121-
m = pattern.search(text)
122-
if m:
123-
c = norm_lang(m.group(1))
124-
if c:
125-
return c
126-
127-
# Fallback
128-
return "en"

libs/rag-core-api/src/rag_core_api/impl/api_endpoints/default_information_pieces_remover.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ def remove_information_piece(self, delete_request: DeleteRequest) -> None:
4848
metadata = {}
4949
for key_value_pair in delete_request.metadata:
5050
metadata["metadata." + key_value_pair.key] = json.loads(key_value_pair.value)
51-
except Exception as e:
52-
logger.error("Error while parsing metadata: %s", e)
51+
except Exception:
52+
logger.exception("Error while parsing metadata")
5353
raise HTTPException(
5454
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
55-
detail="Error while parsing metadata: %s" % e,
55+
detail="Error while parsing metadata.",
5656
)
5757
if not metadata:
5858
raise HTTPException(
@@ -61,9 +61,9 @@ def remove_information_piece(self, delete_request: DeleteRequest) -> None:
6161
)
6262
try:
6363
self._vector_database.delete(metadata)
64-
except Exception as e:
65-
logger.error("Error while deleting from vector db: %s", e)
64+
except Exception:
65+
logger.exception("Error while deleting from vector db")
6666
raise HTTPException(
6767
status_code=status.HTTP_404_NOT_FOUND,
68-
detail="Error while deleting %s from vector db" % delete_request.metadata,
68+
detail="Error while deleting from vector db.",
6969
)

libs/rag-core-api/src/rag_core_api/impl/evaluator/langfuse_ragas_evaluator.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ async def aevaluate(self) -> None:
139139
try:
140140
evaluation_dataset = self._get_dataset(self._settings.evaluation_dataset_name)
141141
await self._aauto_answer_generation4evaluation_questions(evaluation_dataset)
142-
except Exception as e:
143-
logger.error("Failed to evaluate questions: %s", e)
142+
except Exception:
143+
logger.exception("Failed to evaluate questions")
144144

145145
async def _aauto_answer_generation4evaluation_questions(self, dataset) -> tuple[int, Dataset]:
146146
session_id = str(uuid4())
@@ -171,8 +171,8 @@ async def _aevaluate_question(self, item, experiment_name: str, generation_time:
171171
# Use langfuse.start_as_current_generation for generation
172172
try:
173173
response = await self._chat_endpoint.achat(config["metadata"]["session_id"], chat_request)
174-
except Exception as e:
175-
logger.info("Error while answering question %s: %s", item.input, e)
174+
except Exception:
175+
logger.exception("Error while answering question %s", item.input)
176176
response = None
177177
output = {
178178
"answer": response.answer if response else None,

libs/rag-core-api/src/rag_core_api/impl/graph/chat_graph.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ async def _rephrase_node(self, state: dict, config: Optional[RunnableConfig] = N
220220
rephrased_question = await self._rephrasing_chain.ainvoke(chain_input=state, config=config)
221221
# Ensure rephrased_question is a string
222222
rephrased_question = getattr(rephrased_question, "content", rephrased_question)
223-
rephrased_question = rephrased_question.strip() if isinstance(rephrased_question, str) else str(rephrased_question).strip()
223+
rephrased_question = (
224+
rephrased_question.strip() if isinstance(rephrased_question, str) else str(rephrased_question).strip()
225+
)
224226
if not rephrased_question:
225227
rephrased_question = state["question"]
226228
return {"rephrased_question": rephrased_question}
@@ -249,7 +251,7 @@ async def _retrieve_node(self, state: dict) -> dict:
249251
self.FINISH_REASONS: ["NoOrEmptyCollectionError"],
250252
}
251253
except Exception as e:
252-
logger.error("Error while searching for documents in vector database: %s", e)
254+
logger.exception("Error while searching for documents in vector database.")
253255
raise HTTPException(
254256
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
255257
detail="Error while searching for documents in vector database: %s" % e,

libs/rag-core-api/src/rag_core_api/impl/vector_databases/qdrant_database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ async def asearch(self, query: str, search_kwargs: dict, filter_kwargs: dict | N
112112
related_results += self._get_related(res.metadata["related"])
113113
return results + related_results
114114

115-
except Exception as e:
116-
logger.error(f"Search failed: {str(e)}")
115+
except Exception:
116+
logger.exception("Search failed")
117117
raise
118118

119119
def get_specific_document(self, document_id: str) -> list[Document]:

libs/rag-core-api/tests/language_detection_chain_test.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
@pytest.mark.asyncio
1818
async def test_language_detection_returns_iso_code_json():
19+
"""LLM returns strict JSON; chain extracts two-letter ISO code."""
1920
llm = FakeListLLM(responses=['{"language": "de"}'])
2021
mock_langfuse = MagicMock()
2122
manager = MockLangfuseManager(
@@ -40,8 +41,9 @@ async def test_language_detection_returns_iso_code_json():
4041

4142
@pytest.mark.asyncio
4243
async def test_language_detection_fallback_to_en_on_garbage():
44+
"""Non-JSON response triggers fallback to 'en'."""
4345
# LLM returns non-JSON, the chain should fall back to 'en'
44-
llm = FakeListLLM(responses=['I think it is German'])
46+
llm = FakeListLLM(responses=["I think it is German"])
4547
mock_langfuse = MagicMock()
4648
manager = MockLangfuseManager(
4749
langfuse=mock_langfuse,
@@ -65,6 +67,7 @@ async def test_language_detection_fallback_to_en_on_garbage():
6567

6668
@pytest.mark.asyncio
6769
async def test_language_detection_accepts_code_fenced_json():
70+
"""Code-fenced JSON is parsed after stripping fences."""
6871
llm = FakeListLLM(responses=['```json\n{"language": "fr"}\n```'])
6972
mock_langfuse = MagicMock()
7073
manager = MockLangfuseManager(
@@ -89,6 +92,7 @@ async def test_language_detection_accepts_code_fenced_json():
8992

9093
@pytest.mark.asyncio
9194
async def test_language_detection_accepts_locale_variant_and_normalizes():
95+
"""Locale variants like de-DE normalize to base code 'de'."""
9296
llm = FakeListLLM(responses=['{"language": "de-DE"}'])
9397
mock_langfuse = MagicMock()
9498
manager = MockLangfuseManager(
@@ -113,6 +117,7 @@ async def test_language_detection_accepts_locale_variant_and_normalizes():
113117

114118
@pytest.mark.asyncio
115119
async def test_language_detection_handles_single_quoted_jsonish():
120+
"""Single-quoted JSON-ish text is handled via regex fallback."""
116121
llm = FakeListLLM(responses=["{'language': 'es'}"])
117122
mock_langfuse = MagicMock()
118123
manager = MockLangfuseManager(
@@ -137,6 +142,7 @@ async def test_language_detection_handles_single_quoted_jsonish():
137142

138143
@pytest.mark.asyncio
139144
async def test_language_detection_handles_loose_kv_format():
145+
"""Loose key-value format like language: "it" is parsed."""
140146
llm = FakeListLLM(responses=['language: "it"'])
141147
mock_langfuse = MagicMock()
142148
manager = MockLangfuseManager(

libs/rag-core-api/tests/utils_language_parsing_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Tests for rag_core_api.utils.utils language parsing helpers."""
22

3-
import pytest
4-
53
from rag_core_api.utils.utils import norm_lang, strip_code_fences, extract_lang_from_parsed
64

75

86
def test_norm_lang_variants():
7+
"""Ensure various inputs normalize to ISO 639-1 codes or empty string."""
98
assert norm_lang("de") == "de"
109
assert norm_lang("DE") == "de"
1110
assert norm_lang("de-DE") == "de"
@@ -17,11 +16,13 @@ def test_norm_lang_variants():
1716

1817

1918
def test_strip_code_fences():
19+
"""Strip code fences and return clean JSON text."""
2020
fenced = """```json\n{\n \"language\": \"de\"\n}\n```"""
2121
assert strip_code_fences(fenced) == '{\n "language": "de"\n}'
2222

2323

2424
def test_extract_from_parsed():
25+
"""Extract language from parsed structures like dicts and lists."""
2526
assert extract_lang_from_parsed({"language": "de"}) == "de"
2627
assert extract_lang_from_parsed([{"language": "en-US"}]) == "en"
2728
assert extract_lang_from_parsed({}) == ""

0 commit comments

Comments
 (0)