ragaudit-cli 0.3.0__py3-none-any.whl

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.
Files changed (71) hide show
  1. ragaudit/__init__.py +1 -0
  2. ragaudit/adapters/__init__.py +0 -0
  3. ragaudit/adapters/base.py +109 -0
  4. ragaudit/adapters/callable_adapter.py +15 -0
  5. ragaudit/cli.py +540 -0
  6. ragaudit/cli_easy.py +458 -0
  7. ragaudit/cli_v2.py +507 -0
  8. ragaudit/env.py +46 -0
  9. ragaudit/generate/__init__.py +0 -0
  10. ragaudit/generate/pipeline.py +486 -0
  11. ragaudit/generate/prompts.py +116 -0
  12. ragaudit/generate/testset_generator.py +146 -0
  13. ragaudit/generate/text.py +75 -0
  14. ragaudit/ingest/__init__.py +0 -0
  15. ragaudit/ingest/chunker.py +53 -0
  16. ragaudit/ingest/corpus.py +120 -0
  17. ragaudit/ingest/loader.py +49 -0
  18. ragaudit/ingest/versioning.py +92 -0
  19. ragaudit/llm/__init__.py +0 -0
  20. ragaudit/llm/client.py +86 -0
  21. ragaudit/llm/factory.py +70 -0
  22. ragaudit/llm/json_output.py +72 -0
  23. ragaudit/llm/mock.py +552 -0
  24. ragaudit/models/__init__.py +0 -0
  25. ragaudit/models/adapter.py +39 -0
  26. ragaudit/models/document.py +28 -0
  27. ragaudit/models/evaluation.py +70 -0
  28. ragaudit/models/question.py +152 -0
  29. ragaudit/models/run.py +44 -0
  30. ragaudit/models/scoring.py +82 -0
  31. ragaudit/models/testset.py +30 -0
  32. ragaudit/models/tune.py +56 -0
  33. ragaudit/report/__init__.py +0 -0
  34. ragaudit/report/generator.py +155 -0
  35. ragaudit/report/plain_summary.py +122 -0
  36. ragaudit/report/templates/_v2_tooltip.jinja +43 -0
  37. ragaudit/report/templates/generate_report.html.jinja +175 -0
  38. ragaudit/report/templates/report.html.jinja +252 -0
  39. ragaudit/report/templates/style.css +1095 -0
  40. ragaudit/report/templates/tune_report.html.jinja +158 -0
  41. ragaudit/report/templates/v2.css +286 -0
  42. ragaudit/report/v2.py +380 -0
  43. ragaudit/runner/__init__.py +0 -0
  44. ragaudit/runner/ablation.py +46 -0
  45. ragaudit/runner/evaluate.py +325 -0
  46. ragaudit/runner/executor.py +98 -0
  47. ragaudit/runner/locked.py +37 -0
  48. ragaudit/runner/summary.py +45 -0
  49. ragaudit/scaffold.py +252 -0
  50. ragaudit/scoring/__init__.py +0 -0
  51. ragaudit/scoring/correctness.py +94 -0
  52. ragaudit/scoring/groundedness.py +86 -0
  53. ragaudit/scoring/retrieval.py +42 -0
  54. ragaudit/scoring/text_match.py +51 -0
  55. ragaudit/scoring/verdict.py +14 -0
  56. ragaudit/storage/__init__.py +0 -0
  57. ragaudit/storage/db.py +81 -0
  58. ragaudit/storage/lock.py +36 -0
  59. ragaudit/storage/repository.py +216 -0
  60. ragaudit/storage/store.py +536 -0
  61. ragaudit/tune/__init__.py +7 -0
  62. ragaudit/tune/devset.py +16 -0
  63. ragaudit/tune/loop.py +208 -0
  64. ragaudit/tune/search_space.py +140 -0
  65. ragaudit/tune/strategies.py +197 -0
  66. ragaudit/yaml_io.py +199 -0
  67. ragaudit_cli-0.3.0.dist-info/METADATA +429 -0
  68. ragaudit_cli-0.3.0.dist-info/RECORD +71 -0
  69. ragaudit_cli-0.3.0.dist-info/WHEEL +4 -0
  70. ragaudit_cli-0.3.0.dist-info/entry_points.txt +2 -0
  71. ragaudit_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,70 @@
1
+ """Builds the LLM client for v2 commands, and enforces --max-llm-calls budgets.
2
+
3
+ Provider is `mock` (the offline MockLLM) or `openai` (the existing
4
+ OpenAI-compatible LLMClient). It comes from the `--llm` flag, else the
5
+ RAGAUDIT_LLM environment variable, else `openai`, which keeps v1's
6
+ "configure with RAGAUDIT_API_*" behaviour as the default.
7
+ """
8
+
9
+ import os
10
+ from typing import Protocol
11
+
12
+ from ragaudit.llm.client import LLMClient
13
+ from ragaudit.llm.mock import MockLLM
14
+
15
+ PROVIDER_ENV = "RAGAUDIT_LLM"
16
+ PROVIDERS = ("openai", "mock")
17
+
18
+
19
+ class Completer(Protocol):
20
+ model: str
21
+
22
+ def complete(self, system: str, user: str) -> str: ...
23
+
24
+
25
+ class LLMBudgetExceeded(Exception):
26
+ """Raised when a call would exceed --max-llm-calls. Callers stop cleanly and save progress."""
27
+
28
+
29
+ def resolve_provider(provider: str | None = None) -> str:
30
+ resolved = (provider or os.environ.get(PROVIDER_ENV) or "openai").lower()
31
+ if resolved not in PROVIDERS:
32
+ raise ValueError(f"unknown LLM provider {resolved!r}; expected one of {', '.join(PROVIDERS)}")
33
+ return resolved
34
+
35
+
36
+ def make_client(provider: str | None = None, model: str | None = None, mock_model: str = "mock-llm") -> Completer:
37
+ """An LLM client for the given provider. Real clients read RAGAUDIT_API_BASE/_API_KEY (and _MODEL if no model)."""
38
+ if resolve_provider(provider) == "mock":
39
+ return MockLLM(model=model or mock_model)
40
+ return LLMClient(model=model)
41
+
42
+
43
+ class CallBudget:
44
+ """A shared counter of LLM calls with an optional cap."""
45
+
46
+ def __init__(self, max_calls: int | None = None) -> None:
47
+ self.max_calls = max_calls
48
+ self.used = 0
49
+
50
+ @property
51
+ def remaining(self) -> int | None:
52
+ return None if self.max_calls is None else max(self.max_calls - self.used, 0)
53
+
54
+ def spend(self) -> None:
55
+ if self.max_calls is not None and self.used >= self.max_calls:
56
+ raise LLMBudgetExceeded(f"LLM call budget of {self.max_calls} reached")
57
+ self.used += 1
58
+
59
+
60
+ class BudgetedCompleter:
61
+ """Wraps a Completer so every call is counted against a shared CallBudget."""
62
+
63
+ def __init__(self, inner: Completer, budget: CallBudget) -> None:
64
+ self._inner = inner
65
+ self.budget = budget
66
+ self.model = inner.model
67
+
68
+ def complete(self, system: str, user: str) -> str:
69
+ self.budget.spend()
70
+ return self._inner.complete(system, user)
@@ -0,0 +1,72 @@
1
+ """Pull one JSON object out of an LLM reply, tolerating the ways real models get it slightly wrong.
2
+
3
+ Every v2 prompt asks for strict JSON, but local models often wrap it in a
4
+ markdown fence, add a sentence before or after it, put a raw newline inside
5
+ a string, or drop the final closing brace. Each of those used to throw away
6
+ an otherwise good answer. This tries, in order:
7
+
8
+ 1. the reply as is (inside a markdown fence, if there is one);
9
+ 2. the span from the first "{" to the last "}";
10
+ 3. the span from the first "{" with any unclosed brackets closed.
11
+
12
+ Strings are parsed with strict=False, so raw control characters inside a
13
+ string are accepted (callers that need one-line text check that
14
+ themselves). Anything still unparseable returns None.
15
+ """
16
+
17
+ import json
18
+ import re
19
+
20
+ _FENCE = re.compile(r"```(?:json)?\s*(.*?)\s*```", re.DOTALL)
21
+
22
+
23
+ def _close_brackets(text: str) -> str | None:
24
+ """Append the closers for any "{" / "[" left open, or None if the text ends inside a string."""
25
+ stack: list[str] = []
26
+ in_string = escaped = False
27
+ for char in text:
28
+ if in_string:
29
+ if escaped:
30
+ escaped = False
31
+ elif char == "\\":
32
+ escaped = True
33
+ elif char == '"':
34
+ in_string = False
35
+ elif char == '"':
36
+ in_string = True
37
+ elif char in "{[":
38
+ stack.append("}" if char == "{" else "]")
39
+ elif char in "}]":
40
+ if not stack or stack.pop() != char:
41
+ return None
42
+ if in_string:
43
+ return None
44
+ return text + "".join(reversed(stack))
45
+
46
+
47
+ def _candidates(text: str) -> list[str]:
48
+ candidates = [text]
49
+ start, end = text.find("{"), text.rfind("}")
50
+ if start != -1 and end > start:
51
+ candidates.append(text[start : end + 1])
52
+ if start != -1:
53
+ closed = _close_brackets(text[start:].rstrip())
54
+ if closed is not None:
55
+ candidates.append(closed)
56
+ return candidates
57
+
58
+
59
+ def parse_json_object(raw: str) -> dict | None:
60
+ """The first JSON object found in raw, or None."""
61
+ text = raw.strip()
62
+ fenced = _FENCE.search(text)
63
+ if fenced:
64
+ text = fenced.group(1)
65
+ for candidate in _candidates(text):
66
+ try:
67
+ data = json.loads(candidate, strict=False)
68
+ except json.JSONDecodeError:
69
+ continue
70
+ if isinstance(data, dict):
71
+ return data
72
+ return None