rag-failcase-kit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ # Python caches
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+
6
+ # Test and tooling caches
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Build artifacts
19
+ build/
20
+ dist/
21
+ *.egg-info/
22
+
23
+ # Local logs generated by examples
24
+ logs/
25
+
26
+ # Environment files
27
+ .env
28
+ .env.*
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rag-failcase-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: rag-failcase-kit
3
+ Version: 0.1.0
4
+ Summary: Lightweight JSONL tracing for debugging failed RAG answers.
5
+ Project-URL: Homepage, https://github.com/slayer011019/rag-failcase-kit
6
+ Project-URL: Repository, https://github.com/slayer011019/rag-failcase-kit
7
+ Project-URL: Issues, https://github.com/slayer011019/rag-failcase-kit/issues
8
+ Author: rag-failcase-kit contributors
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: debugging,jsonl,logging,rag,retrieval
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Debuggers
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: build>=1.2; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Requires-Dist: twine>=5.0; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # rag-failcase-kit
31
+
32
+ `rag-failcase-kit` is a lightweight Python package for recording one RAG pipeline run as one JSONL trace.
33
+
34
+ When a RAG application returns an empty, inaccurate, or weakly relevant answer, it is often hard to tell where the failure happened. The issue may be retrieval quality, prompt construction, fallback logic, answer generation, or a runtime error. In small projects this often becomes a pile of `print` statements, which is difficult to search, compare, or replay later.
35
+
36
+ This package keeps the first version intentionally small: it records query, retrieved documents, scores, fallback usage, answer preview, latency, status, and errors into a JSONL file.
37
+
38
+ ## Installation
39
+
40
+ From PyPI:
41
+
42
+ ```bash
43
+ pip install rag-failcase-kit
44
+ ```
45
+
46
+ From a local checkout:
47
+
48
+ ```bash
49
+ pip install -e .
50
+ ```
51
+
52
+ For development and release checks:
53
+
54
+ ```bash
55
+ pip install -e ".[dev]"
56
+ ```
57
+
58
+ ## 30-Second Usage
59
+
60
+ ```python
61
+ from rag_failcase_kit import RagLogger
62
+
63
+ logger = RagLogger(project="demo-rag", log_path="logs/rag_traces.jsonl")
64
+
65
+ with logger.trace("What is RAG?") as trace:
66
+ trace.log_retrieval(
67
+ [
68
+ {
69
+ "content": "Retrieval-augmented generation combines retrieval with text generation.",
70
+ "source": "docs/rag.md",
71
+ "score": 0.91,
72
+ }
73
+ ],
74
+ top_k=3,
75
+ min_score=0.7,
76
+ )
77
+ trace.log_answer("RAG retrieves relevant context and uses it to generate an answer.")
78
+ ```
79
+
80
+ The log directory is created automatically. Each trace is appended as one JSON object per line.
81
+
82
+ ## CLI Usage
83
+
84
+ The package installs a `rag-failcase` command:
85
+
86
+ ```bash
87
+ rag-failcase summary logs/rag_traces.jsonl
88
+ rag-failcase failures logs/rag_traces.jsonl
89
+ rag-failcase inspect logs/rag_traces.jsonl --trace-id <trace_id>
90
+ ```
91
+
92
+ All CLI commands print JSON output.
93
+
94
+ Example summary:
95
+
96
+ ```json
97
+ {
98
+ "total_traces": 12,
99
+ "successes": 9,
100
+ "failures": 3,
101
+ "fallback_used": 4,
102
+ "avg_latency_ms": 128.42
103
+ }
104
+ ```
105
+
106
+ ## Log Schema
107
+
108
+ Example JSONL record:
109
+
110
+ ```json
111
+ {
112
+ "trace_id": "9fcd7b91-6b12-46fb-8ff1-5f6f1ef49b15",
113
+ "project": "demo-rag",
114
+ "query": "What is RAG?",
115
+ "retrieval": {
116
+ "documents": [
117
+ {
118
+ "content_preview": "Retrieval-augmented generation combines retrieval with text generation.",
119
+ "metadata": {
120
+ "chunk_id": "rag-001"
121
+ },
122
+ "source": "docs/rag.md",
123
+ "score": 0.91
124
+ }
125
+ ],
126
+ "top_k": 3,
127
+ "min_score": 0.7
128
+ },
129
+ "retrieved_count": 1,
130
+ "fallback": {
131
+ "used": false,
132
+ "strategy": null,
133
+ "reason": null
134
+ },
135
+ "answer": "RAG retrieves relevant context and uses it to generate an answer.",
136
+ "answer_preview": "RAG retrieves relevant context and uses it to generate an answer.",
137
+ "latency_ms": 3.241,
138
+ "status": "success",
139
+ "error": null,
140
+ "created_at": "2026-07-09T00:00:00+00:00"
141
+ }
142
+ ```
143
+
144
+ Document content is stored as a bounded preview to avoid unexpectedly large logs.
145
+
146
+ ## Real RAG Integration
147
+
148
+ Wrap the part of your RAG pipeline that handles one user query:
149
+
150
+ ```python
151
+ from rag_failcase_kit import RagLogger
152
+
153
+ logger = RagLogger(project="support-bot", log_path="logs/support_rag.jsonl")
154
+
155
+
156
+ def answer_question(query, retriever, llm):
157
+ with logger.trace(query) as trace:
158
+ documents = retriever.search(query)
159
+ trace.log_retrieval(documents, top_k=5, min_score=0.75)
160
+
161
+ if not documents:
162
+ trace.log_fallback("no_documents", reason="Retriever returned no documents.")
163
+ answer = "I do not have enough context to answer."
164
+ trace.log_answer(answer)
165
+ return answer
166
+
167
+ answer = llm.generate(query=query, documents=documents)
168
+ trace.log_answer(answer)
169
+ return answer
170
+ ```
171
+
172
+ `documents` can be dictionaries or document-like objects. For objects, the logger safely checks common fields such as `page_content`, `metadata`, `source`, and `score`.
173
+
174
+ ## Optional Integrations
175
+
176
+ The package does not depend on FastAPI, LangChain, or LangGraph.
177
+
178
+ FastAPI example:
179
+
180
+ ```bash
181
+ pip install fastapi uvicorn
182
+ uvicorn examples.fastapi_example:app --reload
183
+ ```
184
+
185
+ LangGraph-style example:
186
+
187
+ ```bash
188
+ python examples/langgraph_example.py
189
+ ```
190
+
191
+ See:
192
+
193
+ - [examples/basic_usage.py](examples/basic_usage.py)
194
+ - [examples/fastapi_example.py](examples/fastapi_example.py)
195
+ - [examples/langgraph_example.py](examples/langgraph_example.py)
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ pip install -e ".[dev]"
201
+ pytest
202
+ python -m build
203
+ twine check dist/*
204
+ ```
205
+
206
+ To test the console script in a clean environment:
207
+
208
+ ```bash
209
+ python -m venv .venv-test
210
+ . .venv-test/bin/activate
211
+ pip install dist/rag_failcase_kit-0.1.0-py3-none-any.whl
212
+ rag-failcase --help
213
+ ```
214
+
215
+ ## Roadmap
216
+
217
+ - Add prompt logging with explicit redaction controls.
218
+ - Add richer failure categorization helpers.
219
+ - Add Markdown or HTML reports.
220
+ - Add log rotation and sampling controls.
221
+ - Add optional adapters for popular RAG frameworks without making them required dependencies.
@@ -0,0 +1,192 @@
1
+ # rag-failcase-kit
2
+
3
+ `rag-failcase-kit` is a lightweight Python package for recording one RAG pipeline run as one JSONL trace.
4
+
5
+ When a RAG application returns an empty, inaccurate, or weakly relevant answer, it is often hard to tell where the failure happened. The issue may be retrieval quality, prompt construction, fallback logic, answer generation, or a runtime error. In small projects this often becomes a pile of `print` statements, which is difficult to search, compare, or replay later.
6
+
7
+ This package keeps the first version intentionally small: it records query, retrieved documents, scores, fallback usage, answer preview, latency, status, and errors into a JSONL file.
8
+
9
+ ## Installation
10
+
11
+ From PyPI:
12
+
13
+ ```bash
14
+ pip install rag-failcase-kit
15
+ ```
16
+
17
+ From a local checkout:
18
+
19
+ ```bash
20
+ pip install -e .
21
+ ```
22
+
23
+ For development and release checks:
24
+
25
+ ```bash
26
+ pip install -e ".[dev]"
27
+ ```
28
+
29
+ ## 30-Second Usage
30
+
31
+ ```python
32
+ from rag_failcase_kit import RagLogger
33
+
34
+ logger = RagLogger(project="demo-rag", log_path="logs/rag_traces.jsonl")
35
+
36
+ with logger.trace("What is RAG?") as trace:
37
+ trace.log_retrieval(
38
+ [
39
+ {
40
+ "content": "Retrieval-augmented generation combines retrieval with text generation.",
41
+ "source": "docs/rag.md",
42
+ "score": 0.91,
43
+ }
44
+ ],
45
+ top_k=3,
46
+ min_score=0.7,
47
+ )
48
+ trace.log_answer("RAG retrieves relevant context and uses it to generate an answer.")
49
+ ```
50
+
51
+ The log directory is created automatically. Each trace is appended as one JSON object per line.
52
+
53
+ ## CLI Usage
54
+
55
+ The package installs a `rag-failcase` command:
56
+
57
+ ```bash
58
+ rag-failcase summary logs/rag_traces.jsonl
59
+ rag-failcase failures logs/rag_traces.jsonl
60
+ rag-failcase inspect logs/rag_traces.jsonl --trace-id <trace_id>
61
+ ```
62
+
63
+ All CLI commands print JSON output.
64
+
65
+ Example summary:
66
+
67
+ ```json
68
+ {
69
+ "total_traces": 12,
70
+ "successes": 9,
71
+ "failures": 3,
72
+ "fallback_used": 4,
73
+ "avg_latency_ms": 128.42
74
+ }
75
+ ```
76
+
77
+ ## Log Schema
78
+
79
+ Example JSONL record:
80
+
81
+ ```json
82
+ {
83
+ "trace_id": "9fcd7b91-6b12-46fb-8ff1-5f6f1ef49b15",
84
+ "project": "demo-rag",
85
+ "query": "What is RAG?",
86
+ "retrieval": {
87
+ "documents": [
88
+ {
89
+ "content_preview": "Retrieval-augmented generation combines retrieval with text generation.",
90
+ "metadata": {
91
+ "chunk_id": "rag-001"
92
+ },
93
+ "source": "docs/rag.md",
94
+ "score": 0.91
95
+ }
96
+ ],
97
+ "top_k": 3,
98
+ "min_score": 0.7
99
+ },
100
+ "retrieved_count": 1,
101
+ "fallback": {
102
+ "used": false,
103
+ "strategy": null,
104
+ "reason": null
105
+ },
106
+ "answer": "RAG retrieves relevant context and uses it to generate an answer.",
107
+ "answer_preview": "RAG retrieves relevant context and uses it to generate an answer.",
108
+ "latency_ms": 3.241,
109
+ "status": "success",
110
+ "error": null,
111
+ "created_at": "2026-07-09T00:00:00+00:00"
112
+ }
113
+ ```
114
+
115
+ Document content is stored as a bounded preview to avoid unexpectedly large logs.
116
+
117
+ ## Real RAG Integration
118
+
119
+ Wrap the part of your RAG pipeline that handles one user query:
120
+
121
+ ```python
122
+ from rag_failcase_kit import RagLogger
123
+
124
+ logger = RagLogger(project="support-bot", log_path="logs/support_rag.jsonl")
125
+
126
+
127
+ def answer_question(query, retriever, llm):
128
+ with logger.trace(query) as trace:
129
+ documents = retriever.search(query)
130
+ trace.log_retrieval(documents, top_k=5, min_score=0.75)
131
+
132
+ if not documents:
133
+ trace.log_fallback("no_documents", reason="Retriever returned no documents.")
134
+ answer = "I do not have enough context to answer."
135
+ trace.log_answer(answer)
136
+ return answer
137
+
138
+ answer = llm.generate(query=query, documents=documents)
139
+ trace.log_answer(answer)
140
+ return answer
141
+ ```
142
+
143
+ `documents` can be dictionaries or document-like objects. For objects, the logger safely checks common fields such as `page_content`, `metadata`, `source`, and `score`.
144
+
145
+ ## Optional Integrations
146
+
147
+ The package does not depend on FastAPI, LangChain, or LangGraph.
148
+
149
+ FastAPI example:
150
+
151
+ ```bash
152
+ pip install fastapi uvicorn
153
+ uvicorn examples.fastapi_example:app --reload
154
+ ```
155
+
156
+ LangGraph-style example:
157
+
158
+ ```bash
159
+ python examples/langgraph_example.py
160
+ ```
161
+
162
+ See:
163
+
164
+ - [examples/basic_usage.py](examples/basic_usage.py)
165
+ - [examples/fastapi_example.py](examples/fastapi_example.py)
166
+ - [examples/langgraph_example.py](examples/langgraph_example.py)
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ pip install -e ".[dev]"
172
+ pytest
173
+ python -m build
174
+ twine check dist/*
175
+ ```
176
+
177
+ To test the console script in a clean environment:
178
+
179
+ ```bash
180
+ python -m venv .venv-test
181
+ . .venv-test/bin/activate
182
+ pip install dist/rag_failcase_kit-0.1.0-py3-none-any.whl
183
+ rag-failcase --help
184
+ ```
185
+
186
+ ## Roadmap
187
+
188
+ - Add prompt logging with explicit redaction controls.
189
+ - Add richer failure categorization helpers.
190
+ - Add Markdown or HTML reports.
191
+ - Add log rotation and sampling controls.
192
+ - Add optional adapters for popular RAG frameworks without making them required dependencies.
@@ -0,0 +1,20 @@
1
+ """Basic usage example for rag-failcase-kit."""
2
+
3
+ from rag_failcase_kit import RagLogger
4
+
5
+
6
+ logger = RagLogger(project="demo-rag", log_path="logs/rag_traces.jsonl")
7
+
8
+ with logger.trace("What is RAG?") as trace:
9
+ trace.log_retrieval(
10
+ [
11
+ {
12
+ "content": "Retrieval-augmented generation combines retrieval with text generation.",
13
+ "source": "docs/rag.md",
14
+ "score": 0.91,
15
+ }
16
+ ],
17
+ top_k=3,
18
+ min_score=0.7,
19
+ )
20
+ trace.log_answer("RAG retrieves relevant context and uses it to generate an answer.")
@@ -0,0 +1,33 @@
1
+ """FastAPI integration example.
2
+
3
+ Install FastAPI separately to run this file:
4
+
5
+ pip install fastapi uvicorn
6
+ uvicorn examples.fastapi_example:app --reload
7
+ """
8
+
9
+ from fastapi import FastAPI
10
+
11
+ from rag_failcase_kit import RagLogger
12
+
13
+
14
+ app = FastAPI()
15
+ logger = RagLogger(project="fastapi-rag", log_path="logs/fastapi_rag.jsonl")
16
+
17
+
18
+ @app.get("/ask")
19
+ def ask(query: str) -> dict:
20
+ """Record one trace around a minimal RAG-like request."""
21
+
22
+ with logger.trace(query) as trace:
23
+ documents = [{"content": "FastAPI is a Python web framework.", "score": 0.88}]
24
+ trace.log_retrieval(documents, top_k=1)
25
+
26
+ if not documents:
27
+ trace.log_fallback("no_context", reason="Retriever returned no documents.")
28
+ answer = "I do not have enough context to answer."
29
+ else:
30
+ answer = "FastAPI is commonly used to build Python APIs."
31
+
32
+ trace.log_answer(answer)
33
+ return {"answer": answer, "trace_id": trace.trace_id}
@@ -0,0 +1,38 @@
1
+ """LangGraph-style integration example without importing LangGraph.
2
+
3
+ The package does not depend on LangGraph. This example shows where tracing can
4
+ be placed inside graph node functions.
5
+ """
6
+
7
+ from typing import Dict, List, TypedDict
8
+
9
+ from rag_failcase_kit import RagLogger
10
+
11
+
12
+ class State(TypedDict, total=False):
13
+ query: str
14
+ documents: List[Dict[str, object]]
15
+ answer: str
16
+ trace_id: str
17
+
18
+
19
+ logger = RagLogger(project="langgraph-rag", log_path="logs/langgraph_rag.jsonl")
20
+
21
+
22
+ def rag_node(state: State) -> State:
23
+ """Example node that records retrieval and answer generation."""
24
+
25
+ query = state["query"]
26
+ with logger.trace(query) as trace:
27
+ documents = [{"content": "Graph nodes can share state.", "score": 0.83}]
28
+ trace.log_retrieval(documents, top_k=2)
29
+
30
+ answer = "A graph node can log retrieval and answer events inside one trace."
31
+ trace.log_answer(answer)
32
+
33
+ return {
34
+ **state,
35
+ "documents": documents,
36
+ "answer": answer,
37
+ "trace_id": trace.trace_id,
38
+ }
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rag-failcase-kit"
7
+ version = "0.1.0"
8
+ description = "Lightweight JSONL tracing for debugging failed RAG answers."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "rag-failcase-kit contributors" },
14
+ ]
15
+ keywords = ["rag", "logging", "debugging", "jsonl", "retrieval"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Software Development :: Debuggers",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/slayer011019/rag-failcase-kit"
33
+ Repository = "https://github.com/slayer011019/rag-failcase-kit"
34
+ Issues = "https://github.com/slayer011019/rag-failcase-kit/issues"
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "build>=1.2",
39
+ "pytest>=7.0",
40
+ "twine>=5.0",
41
+ ]
42
+
43
+ [project.scripts]
44
+ rag-failcase = "rag_failcase_kit.cli:main"
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+ pythonpath = ["src"]
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/rag_failcase_kit"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = [
55
+ "README.md",
56
+ "LICENSE",
57
+ "pyproject.toml",
58
+ "src",
59
+ "examples",
60
+ "tests",
61
+ ]
@@ -0,0 +1,8 @@
1
+ """Small JSONL tracing toolkit for RAG failure analysis."""
2
+
3
+ from rag_failcase_kit.logger import RagLogger
4
+ from rag_failcase_kit.trace import RagTrace
5
+
6
+ __all__ = ["RagLogger", "RagTrace"]
7
+
8
+ __version__ = "0.1.0"
@@ -0,0 +1,74 @@
1
+ """Command line interface for rag-failcase-kit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ from rag_failcase_kit.report import failure_records, find_record, load_records, summarize
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ """Build the CLI argument parser."""
15
+
16
+ parser = argparse.ArgumentParser(
17
+ prog="rag-failcase",
18
+ description="Inspect rag-failcase-kit JSONL trace logs.",
19
+ )
20
+ subparsers = parser.add_subparsers(dest="command", required=True)
21
+
22
+ summary_parser = subparsers.add_parser("summary", help="Show aggregate log statistics.")
23
+ summary_parser.add_argument("log_path")
24
+
25
+ failures_parser = subparsers.add_parser("failures", help="List failed traces.")
26
+ failures_parser.add_argument("log_path")
27
+
28
+ inspect_parser = subparsers.add_parser("inspect", help="Show one trace by ID.")
29
+ inspect_parser.add_argument("log_path")
30
+ inspect_parser.add_argument("--trace-id", required=True)
31
+
32
+ return parser
33
+
34
+
35
+ def main(argv: Optional[List[str]] = None) -> int:
36
+ """Run the command line interface."""
37
+
38
+ parser = build_parser()
39
+ args = parser.parse_args(argv)
40
+
41
+ try:
42
+ records = load_records(args.log_path)
43
+ except FileNotFoundError as exc:
44
+ print(str(exc), file=sys.stderr)
45
+ return 1
46
+
47
+ if args.command == "summary":
48
+ _print_json(summarize(records))
49
+ return 0
50
+
51
+ if args.command == "failures":
52
+ _print_json(failure_records(records))
53
+ return 0
54
+
55
+ if args.command == "inspect":
56
+ record = find_record(records, args.trace_id)
57
+ if record is None:
58
+ print(f"Trace not found: {args.trace_id}", file=sys.stderr)
59
+ return 1
60
+ _print_json(record)
61
+ return 0
62
+
63
+ parser.print_help()
64
+ return 1
65
+
66
+
67
+ def _print_json(value: Union[Dict[str, Any], List[Dict[str, Any]]]) -> None:
68
+ """Print JSON output for humans and scripts."""
69
+
70
+ print(json.dumps(value, indent=2, ensure_ascii=False))
71
+
72
+
73
+ if __name__ == "__main__":
74
+ raise SystemExit(main())
@@ -0,0 +1,28 @@
1
+ """Public logger facade for creating RAG traces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Union
7
+
8
+ from rag_failcase_kit.trace import RagTrace
9
+ from rag_failcase_kit.writer import JsonlWriter
10
+
11
+
12
+ class RagLogger:
13
+ """Create structured JSONL traces for RAG runs.
14
+
15
+ Args:
16
+ project: Human-readable project or application name.
17
+ log_path: JSONL file path where traces will be appended.
18
+ """
19
+
20
+ def __init__(self, project: str, log_path: Union[str, Path] = "logs/rag_traces.jsonl") -> None:
21
+ self.project = project
22
+ self.log_path = Path(log_path)
23
+ self.writer = JsonlWriter(self.log_path)
24
+
25
+ def trace(self, query: str) -> RagTrace:
26
+ """Create a context manager for one RAG query execution."""
27
+
28
+ return RagTrace(project=self.project, query=query, writer=self.writer)
@@ -0,0 +1,59 @@
1
+ """Read and summarize JSONL trace logs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Iterable, List, Optional, Union
8
+
9
+
10
+ def load_records(log_path: Union[str, Path]) -> List[Dict[str, Any]]:
11
+ """Load trace records from a JSONL file."""
12
+
13
+ path = Path(log_path)
14
+ if not path.exists():
15
+ raise FileNotFoundError(f"Log file not found: {path}")
16
+
17
+ records: List[Dict[str, Any]] = []
18
+ with path.open("r", encoding="utf-8") as file:
19
+ for line in file:
20
+ if line.strip():
21
+ records.append(json.loads(line))
22
+ return records
23
+
24
+
25
+ def summarize(records: Iterable[Dict[str, Any]]) -> Dict[str, Any]:
26
+ """Return aggregate statistics for trace records."""
27
+
28
+ items = list(records)
29
+ total = len(items)
30
+ failures = [record for record in items if record.get("status") != "success"]
31
+ fallback_used = [record for record in items if record.get("fallback", {}).get("used")]
32
+ latencies = [
33
+ record["latency_ms"]
34
+ for record in items
35
+ if isinstance(record.get("latency_ms"), (int, float))
36
+ ]
37
+
38
+ return {
39
+ "total_traces": total,
40
+ "successes": total - len(failures),
41
+ "failures": len(failures),
42
+ "fallback_used": len(fallback_used),
43
+ "avg_latency_ms": round(sum(latencies) / len(latencies), 3) if latencies else None,
44
+ }
45
+
46
+
47
+ def failure_records(records: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
48
+ """Return records that indicate a failed or errored trace."""
49
+
50
+ return [record for record in records if record.get("status") != "success"]
51
+
52
+
53
+ def find_record(records: Iterable[Dict[str, Any]], trace_id: str) -> Optional[Dict[str, Any]]:
54
+ """Find a trace by ID."""
55
+
56
+ for record in records:
57
+ if record.get("trace_id") == trace_id:
58
+ return record
59
+ return None
@@ -0,0 +1,120 @@
1
+ """Schema helpers for trace records.
2
+
3
+ The project intentionally uses plain dictionaries instead of a validation
4
+ dependency. This keeps the MVP lightweight and easy to inspect.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ TraceRecord = Dict[str, Any]
14
+ DocumentRecord = Dict[str, Any]
15
+
16
+
17
+ def utc_now_iso() -> str:
18
+ """Return the current UTC time as an ISO-8601 string."""
19
+
20
+ return datetime.now(timezone.utc).isoformat()
21
+
22
+
23
+ def new_trace_record(
24
+ *,
25
+ trace_id: str,
26
+ project: str,
27
+ query: str,
28
+ created_at: Optional[str] = None,
29
+ ) -> TraceRecord:
30
+ """Create the default JSON-serializable record for one RAG trace."""
31
+
32
+ return {
33
+ "trace_id": trace_id,
34
+ "project": project,
35
+ "query": query,
36
+ "retrieval": {
37
+ "documents": [],
38
+ "top_k": None,
39
+ "min_score": None,
40
+ },
41
+ "retrieved_count": 0,
42
+ "fallback": {
43
+ "used": False,
44
+ "strategy": None,
45
+ "reason": None,
46
+ },
47
+ "answer": None,
48
+ "answer_preview": None,
49
+ "latency_ms": None,
50
+ "status": "running",
51
+ "error": None,
52
+ "created_at": created_at or utc_now_iso(),
53
+ }
54
+
55
+
56
+ def preview_text(value: Any, *, limit: int = 500) -> Optional[str]:
57
+ """Convert a value to a bounded text preview."""
58
+
59
+ if value is None:
60
+ return None
61
+
62
+ text = str(value)
63
+ if len(text) <= limit:
64
+ return text
65
+ return text[:limit] + "..."
66
+
67
+
68
+ def extract_documents(documents: List[Any], *, content_limit: int = 500) -> List[DocumentRecord]:
69
+ """Normalize dictionaries or document-like objects into safe log records.
70
+
71
+ Supported object attributes are intentionally broad enough for common RAG
72
+ libraries without importing them: page_content, metadata, source, and score.
73
+ """
74
+
75
+ return [extract_document(document, content_limit=content_limit) for document in documents]
76
+
77
+
78
+ def extract_document(document: Any, *, content_limit: int = 500) -> DocumentRecord:
79
+ """Normalize one document-like value into a JSON-serializable dictionary."""
80
+
81
+ if isinstance(document, dict):
82
+ content = (
83
+ document.get("content")
84
+ or document.get("page_content")
85
+ or document.get("text")
86
+ or document.get("body")
87
+ )
88
+ metadata = document.get("metadata") or {}
89
+ source = document.get("source")
90
+ if source is None and isinstance(metadata, dict):
91
+ source = metadata.get("source")
92
+ score = document.get("score")
93
+ if score is None:
94
+ score = document.get("similarity_score")
95
+ else:
96
+ content = getattr(document, "page_content", None) or getattr(document, "content", None)
97
+ metadata = getattr(document, "metadata", {}) or {}
98
+ source = getattr(document, "source", None)
99
+ if source is None and isinstance(metadata, dict):
100
+ source = metadata.get("source")
101
+ score = getattr(document, "score", None)
102
+
103
+ return {
104
+ "content_preview": preview_text(content, limit=content_limit),
105
+ "metadata": make_json_safe(metadata),
106
+ "source": source,
107
+ "score": score,
108
+ }
109
+
110
+
111
+ def make_json_safe(value: Any) -> Any:
112
+ """Convert common Python values into JSON-safe structures."""
113
+
114
+ if value is None or isinstance(value, (str, int, float, bool)):
115
+ return value
116
+ if isinstance(value, dict):
117
+ return {str(key): make_json_safe(item) for key, item in value.items()}
118
+ if isinstance(value, (list, tuple, set)):
119
+ return [make_json_safe(item) for item in value]
120
+ return str(value)
@@ -0,0 +1,112 @@
1
+ """Trace object used to record one RAG execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import traceback
6
+ from time import perf_counter
7
+ from typing import Any, List, Optional
8
+ from uuid import uuid4
9
+
10
+ from rag_failcase_kit.schema import extract_documents, new_trace_record, preview_text
11
+ from rag_failcase_kit.writer import JsonlWriter
12
+
13
+
14
+ class RagTrace:
15
+ """Collect structured events for one RAG pipeline run."""
16
+
17
+ def __init__(self, *, project: str, query: str, writer: JsonlWriter) -> None:
18
+ self.project = project
19
+ self.query = query
20
+ self.writer = writer
21
+ self.trace_id = str(uuid4())
22
+ self.record = new_trace_record(
23
+ trace_id=self.trace_id,
24
+ project=self.project,
25
+ query=self.query,
26
+ )
27
+ self._started_at = perf_counter()
28
+ self._finished = False
29
+
30
+ def __enter__(self) -> "RagTrace":
31
+ """Enter the trace context manager."""
32
+
33
+ return self
34
+
35
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
36
+ """Finish the trace when leaving a context manager.
37
+
38
+ Exceptions are logged and then allowed to propagate.
39
+ """
40
+
41
+ if exc is not None:
42
+ self.log_error(exc)
43
+ self.finish(status="error")
44
+ return False
45
+
46
+ if not self._finished:
47
+ self.finish()
48
+ return False
49
+
50
+ def log_retrieval(
51
+ self,
52
+ documents: List[Any],
53
+ top_k: Optional[int] = None,
54
+ min_score: Optional[float] = None,
55
+ ) -> None:
56
+ """Log retrieved documents and retrieval parameters."""
57
+
58
+ normalized_documents = extract_documents(documents)
59
+ self.record["retrieval"] = {
60
+ "documents": normalized_documents,
61
+ "top_k": top_k,
62
+ "min_score": min_score,
63
+ }
64
+ self.record["retrieved_count"] = len(normalized_documents)
65
+
66
+ def log_fallback(self, strategy: str, reason: Optional[str] = None) -> None:
67
+ """Log that fallback behavior was used."""
68
+
69
+ self.record["fallback"] = {
70
+ "used": True,
71
+ "strategy": strategy,
72
+ "reason": reason,
73
+ }
74
+
75
+ def log_answer(self, answer: Any) -> None:
76
+ """Log the generated answer and a bounded preview."""
77
+
78
+ self.record["answer"] = answer
79
+ self.record["answer_preview"] = preview_text(answer)
80
+
81
+ def log_error(self, error: Any) -> None:
82
+ """Log an error raised or observed during the RAG run."""
83
+
84
+ if isinstance(error, BaseException):
85
+ formatted_traceback = None
86
+ if error.__traceback__ is not None:
87
+ formatted_traceback = "".join(
88
+ traceback.format_exception(type(error), error, error.__traceback__)
89
+ )
90
+ self.record["error"] = {
91
+ "type": error.__class__.__name__,
92
+ "message": str(error),
93
+ "traceback": formatted_traceback,
94
+ }
95
+ else:
96
+ self.record["error"] = {
97
+ "type": type(error).__name__,
98
+ "message": str(error),
99
+ "traceback": None,
100
+ }
101
+
102
+ def finish(self, status: str = "success") -> None:
103
+ """Finalize and write the trace exactly once."""
104
+
105
+ if self._finished:
106
+ return
107
+
108
+ elapsed_ms = (perf_counter() - self._started_at) * 1000
109
+ self.record["latency_ms"] = round(elapsed_ms, 3)
110
+ self.record["status"] = status
111
+ self.writer.write(self.record)
112
+ self._finished = True
@@ -0,0 +1,22 @@
1
+ """JSONL writer for trace records."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Union
8
+
9
+
10
+ class JsonlWriter:
11
+ """Append JSON records to a JSONL file."""
12
+
13
+ def __init__(self, log_path: Union[str, Path]) -> None:
14
+ self.log_path = Path(log_path)
15
+ self.log_path.parent.mkdir(parents=True, exist_ok=True)
16
+
17
+ def write(self, record: Dict[str, Any]) -> None:
18
+ """Append one JSON-serializable record as a single line."""
19
+
20
+ with self.log_path.open("a", encoding="utf-8") as file:
21
+ json.dump(record, file, ensure_ascii=False, default=str)
22
+ file.write("\n")
@@ -0,0 +1,26 @@
1
+ import json
2
+
3
+ from rag_failcase_kit.cli import main
4
+
5
+
6
+ def test_cli_summary_outputs_counts(tmp_path, capsys):
7
+ log_path = tmp_path / "traces.jsonl"
8
+ records = [
9
+ {"trace_id": "1", "status": "success", "fallback": {"used": False}, "latency_ms": 10},
10
+ {"trace_id": "2", "status": "error", "fallback": {"used": True}, "latency_ms": 30},
11
+ ]
12
+ log_path.write_text(
13
+ "\n".join(json.dumps(record) for record in records) + "\n",
14
+ encoding="utf-8",
15
+ )
16
+
17
+ exit_code = main(["summary", str(log_path)])
18
+
19
+ captured = capsys.readouterr()
20
+ summary = json.loads(captured.out)
21
+ assert exit_code == 0
22
+ assert summary["total_traces"] == 2
23
+ assert summary["successes"] == 1
24
+ assert summary["failures"] == 1
25
+ assert summary["fallback_used"] == 1
26
+ assert summary["avg_latency_ms"] == 20
@@ -0,0 +1,11 @@
1
+ from rag_failcase_kit import RagLogger
2
+
3
+
4
+ def test_logger_initialization_creates_log_directory(tmp_path):
5
+ log_path = tmp_path / "nested" / "traces.jsonl"
6
+
7
+ logger = RagLogger(project="test-project", log_path=log_path)
8
+
9
+ assert logger.project == "test-project"
10
+ assert logger.log_path == log_path
11
+ assert log_path.parent.exists()
@@ -0,0 +1,77 @@
1
+ import json
2
+
3
+ from rag_failcase_kit import RagLogger
4
+
5
+
6
+ def read_first_record(log_path):
7
+ return json.loads(log_path.read_text(encoding="utf-8").splitlines()[0])
8
+
9
+
10
+ def test_trace_saves_jsonl_record(tmp_path):
11
+ log_path = tmp_path / "traces.jsonl"
12
+ logger = RagLogger(project="demo", log_path=log_path)
13
+
14
+ with logger.trace("hello") as trace:
15
+ trace.log_answer("world")
16
+
17
+ record = read_first_record(log_path)
18
+ assert record["project"] == "demo"
19
+ assert record["query"] == "hello"
20
+ assert record["answer_preview"] == "world"
21
+ assert record["status"] == "success"
22
+ assert record["trace_id"]
23
+ assert isinstance(record["latency_ms"], float)
24
+
25
+
26
+ def test_retrieval_logging_handles_dicts_and_objects(tmp_path):
27
+ class Document:
28
+ page_content = "object document content"
29
+ metadata = {"source": "object.md"}
30
+ score = 0.81
31
+
32
+ log_path = tmp_path / "traces.jsonl"
33
+ logger = RagLogger(project="demo", log_path=log_path)
34
+
35
+ with logger.trace("query") as trace:
36
+ trace.log_retrieval(
37
+ [
38
+ {"content": "dict document content", "source": "dict.md", "score": 0.95},
39
+ Document(),
40
+ ],
41
+ top_k=2,
42
+ min_score=0.8,
43
+ )
44
+
45
+ record = read_first_record(log_path)
46
+ assert record["retrieved_count"] == 2
47
+ assert record["retrieval"]["top_k"] == 2
48
+ assert record["retrieval"]["min_score"] == 0.8
49
+ assert record["retrieval"]["documents"][0]["content_preview"] == "dict document content"
50
+ assert record["retrieval"]["documents"][1]["source"] == "object.md"
51
+
52
+
53
+ def test_fallback_logging(tmp_path):
54
+ log_path = tmp_path / "traces.jsonl"
55
+ logger = RagLogger(project="demo", log_path=log_path)
56
+
57
+ with logger.trace("query") as trace:
58
+ trace.log_fallback("no_results", reason="Retriever returned nothing.")
59
+
60
+ record = read_first_record(log_path)
61
+ assert record["fallback"]["used"] is True
62
+ assert record["fallback"]["strategy"] == "no_results"
63
+ assert record["fallback"]["reason"] == "Retriever returned nothing."
64
+
65
+
66
+ def test_error_logging_with_explicit_finish(tmp_path):
67
+ log_path = tmp_path / "traces.jsonl"
68
+ logger = RagLogger(project="demo", log_path=log_path)
69
+
70
+ with logger.trace("query") as trace:
71
+ trace.log_error(ValueError("bad answer"))
72
+ trace.finish(status="error")
73
+
74
+ record = read_first_record(log_path)
75
+ assert record["status"] == "error"
76
+ assert record["error"]["type"] == "ValueError"
77
+ assert record["error"]["message"] == "bad answer"
@@ -0,0 +1,14 @@
1
+ import json
2
+
3
+ from rag_failcase_kit.writer import JsonlWriter
4
+
5
+
6
+ def test_writer_appends_single_json_line(tmp_path):
7
+ log_path = tmp_path / "logs" / "trace.jsonl"
8
+ writer = JsonlWriter(log_path)
9
+
10
+ writer.write({"trace_id": "abc", "status": "success"})
11
+
12
+ lines = log_path.read_text(encoding="utf-8").splitlines()
13
+ assert len(lines) == 1
14
+ assert json.loads(lines[0]) == {"trace_id": "abc", "status": "success"}