import logging
import os
from collections.abc import Callable
-from sys import stderr
import fire
import pydantic
-from pydantic_core import ValidationError
from tqdm import tqdm
from rag.answering import InferenceBackend
from rag.chunking import FileType, chunk_file
+from rag.evaluate import sources_overlap
from rag.models import (
+ AnswerSet,
RagDataset,
RetrievedQuestion,
SearchAnswers,
with open(path) as f:
return cb(f.read())
except Exception as e:
- logging.getLogger(__name__).error(f"Failed to read {name}: {e}")
+ logging.getLogger(__name__).error(
+ f"Failed to read {name} ({path}): {e}"
+ )
exit(1)
@staticmethod
with open(path, "w") as f:
f.write(data)
except Exception as e:
- logging.getLogger(__name__).error(f"Failed to write {name}: {e}")
+ logging.getLogger(__name__).error(
+ f"Failed to write {name} ({path}): {e}"
+ )
exit(1)
def index(
index: str = "data/processed",
max_chunk_size: int = 2000,
) -> None:
+ if max_chunk_size < 1:
+ logging.getLogger(__name__).error(
+ f"Invalid max chunk size {max_chunk_size}"
+ )
+ exit(1)
chunks = {}
for dirpath, _, files in tqdm(
- list(os.walk(data)), desc="Collecting files"
+ list(os.walk(data)), desc="Chunking files"
):
for file in files:
path = dirpath + "/" + file
def search(
self, query: str, /, k: int = 5, index: str = "data/processed"
) -> None:
+ if k < 1:
+ logging.getLogger(__name__).error(f"Invalid k {k}")
+ exit(1)
for source in self._search(query, k, index):
print(source)
k: int = 5,
index: str = "data/processed",
) -> None:
+ if k < 1:
+ logging.getLogger(__name__).error(f"Invalid k {k}")
+ exit(1)
bm25 = self._bm25(index)
dataset = self._readf(dataset_path, RagDataset.model_validate_json)
result = SearchResults(
index: str = "data/processed",
max_tokens: int = 100,
) -> None:
+ if k < 1:
+ logging.getLogger(__name__).error(f"Invalid k {k}")
+ exit(1)
+ if max_tokens < 1:
+ logging.getLogger(__name__).error(
+ f"Invalid max tokens {max_tokens}"
+ )
+ exit(1)
sources = self._search(query, k, index)
inference = self._inference()
print("Sources:")
student_search_result_path: str,
save_directory: str,
/,
- index: str = "data/processed",
max_tokens: int = 100,
) -> None:
- bm25 = self._bm25(index)
+ if max_tokens < 1:
+ logging.getLogger(__name__).error(
+ f"Invalid max tokens {max_tokens}"
+ )
+ exit(1)
dataset = self._readf(
student_search_result_path,
SearchResults.model_validate_json,
+ os.path.basename(student_search_result_path),
result.model_dump_json(),
)
+
+ def _evaluate(
+ self, answers: SearchResults, references: AnswerSet, k: int, iou: float
+ ) -> None:
+ count = len(references.rag_questions)
+ success = sum(
+ 1
+ for answer, reference in zip(
+ answers.search_results,
+ references.rag_questions,
+ strict=False,
+ )
+ if any(
+ sources_overlap(a, b, iou)
+ for i, a in enumerate(answer.retrieved_sources)
+ for b in reference.sources
+ if i < k
+ )
+ )
+ print(
+ f"recall@{k}: {success / count * 100.0:.2f}% ({success} / {count})"
+ )
+
+ def evaluate(
+ self,
+ student_search_result_path: str,
+ dataset_path: str,
+ /,
+ k: int = 5,
+ iou: float = 0.05,
+ ) -> None:
+ if k < 1:
+ logging.getLogger(__name__).error(f"Invalid k {k}")
+ exit(1)
+ answers = self._readf(
+ student_search_result_path,
+ SearchResults.model_validate_json,
+ name="student search results",
+ )
+ references = self._readf(
+ dataset_path,
+ AnswerSet.model_validate_json,
+ name="reference dataset",
+ )
+ if len(answers.search_results) != len(references.rag_questions):
+ print("Mismatch between search result and reference set lenghts!")
+ exit(1)
+ if len(answers.search_results) == 0:
+ print("Empty dataset, grade would not make sense.")
+ exit(0)
+ prev_i = 1
+ i = 1
+ while i < k:
+ self._evaluate(answers, references, i, iou)
+ prev_i, i = i, i + prev_i
+ self._evaluate(answers, references, k, iou)
from collections.abc import Callable
from typing import Any, cast
-from transformers import AutoModelForCausalLM, AutoTokenizer
-from transformers.generation import BaseStreamer # type:ignore
-
from rag.models import AnsweredQuestion, RetrievedQuestion, Source
do not mention them.
End your answer with a newline."""
- class _Streamer(BaseStreamer):
- def __init__(
- self, backend: "InferenceBackend", cb: Callable[[str], None]
- ) -> None:
- self.backend = backend
- self.cb = cb
- self.skip = True
-
- def put(self, value: Any) -> None:
- if self.skip:
- self.skip = False
- return
- s: str = cast(
- str,
- self.backend.tokenizer.decode(value, skip_special_tokens=True),
- )
- self.cb(s.rstrip("\n"))
-
- def end(self) -> None:
- self.cb("\n")
-
def __init__(self, model: str) -> None:
+ from transformers import AutoModelForCausalLM, AutoTokenizer
+
self.model: Any = AutoModelForCausalLM.from_pretrained(
model, device_map="auto"
)
max_tokens: int,
cb: Callable[[str], None],
) -> str:
+ from transformers.generation import BaseStreamer # type: ignore
+
+ class Streamer(BaseStreamer):
+ def __init__(
+ self, backend: "InferenceBackend", cb: Callable[[str], None]
+ ) -> None:
+ self.backend = backend
+ self.cb = cb
+ self.skip = True
+
+ def put(self, value: Any) -> None:
+ if self.skip:
+ self.skip = False
+ return
+ s: str = cast(
+ str,
+ self.backend.tokenizer.decode(
+ value, skip_special_tokens=True
+ ),
+ )
+ self.cb(s.rstrip("\n"))
+
+ def end(self) -> None:
+ self.cb("\n")
+
prompt = self._prompt(question)
model_inputs = self.tokenizer([prompt], return_tensors="pt").to(
self.model.device
max_new_tokens=max_tokens,
stop_strings="\n",
tokenizer=self.tokenizer,
- streamer=self._Streamer(self, cb),
+ streamer=Streamer(self, cb),
)
full_response = self.tokenizer.batch_decode(
generated_ids,