Skip to content

Commit ccf986b

Browse files
author
MrCabss69
committed
Merge branch 'master' into DAGE-18/Persistent-Storage-JSON
2 parents fb26d0b + d08bee8 commit ccf986b

22 files changed

Lines changed: 284 additions & 105 deletions

.github/workflows/test.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Unit Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ main, dataset-generator ]
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout code
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v4
17+
with:
18+
python-version: "3.11"
19+
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@v3
22+
with:
23+
version: "latest"
24+
25+
- name: Install dependencies
26+
run: uv sync
27+
working-directory: rre-dataset-generator
28+
29+
- name: Run unit tests
30+
run: uv run pytest
31+
working-directory: rre-dataset-generator

rre-dataset-generator/config.yaml

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,28 @@
22
# dataset-generator module
33
# Default: "q=#$query##"
44
# Example (solr):
5-
query_template: "q=#$query##&fq=genre:horror&wt=json"
5+
query_template: "q=#$query##&df=description&rows=3&q.op=OR&wt=json"
66

77
# The type of search engine to use
88
# Accepted values: solr, elasticsearch, opensearch, vespa
99
search_engine_type: "solr"
1010

1111
# Endpoint URL of the search engine collection or index
12-
search_engine_collection_endpoint: "http://localhost:8983/solr/mycore"
12+
search_engine_collection_endpoint: "http://localhost:8983/solr/testcore/"
1313

1414
# (Optional) Filter query to restrict the set of documents used to generate queries.
1515
# If a field has more than one value, the documents retrieved have at least one of the values in the field (OR-
1616
# -like). If there are more than one fields, the documents are filtered for both fields (AND-like)
1717
# Default: all documents are considered and no filter is applied
18-
documents_filter:
19-
- genre:
20-
- "horror"
21-
- "fantasy"
22-
- type:
23-
- "book"
18+
#documents_filter:
19+
# - genre:
20+
# - "horror"
21+
# - "fantasy"
22+
# - type:
23+
# - "book"
2424

2525
# Maximum number of documents to retrieve from the search engine to generate queries
26-
doc_number: 100
26+
doc_number: 2
2727

2828
# List of fields from documents used to generate queries and relevance scoring
2929
# These should match fields available in the search engine schema
@@ -34,14 +34,14 @@ doc_fields:
3434

3535
# (Optional) File containing predefined queries to use instead of or alongside generated ones
3636
# If available, must be in a txt format.
37-
queries: "queries.txt"
37+
#queries: "queries.txt"
3838

3939
# (Optional) Whether to generate queries from documents
4040
# Default: true; set to false to disable query generation from documents
4141
generate_queries_from_documents: true
4242

4343
# Total number of queries to generate (includes predefined queries, if any)
44-
num_queries_needed: 10
44+
num_queries_needed: 4
4545

4646
# Relevance scale used for scoring document relevance
4747
# Accepted values:
@@ -57,4 +57,4 @@ llm_configuration_file: "llm_config.yaml"
5757
output_format: "quepid"
5858

5959
# Path where the output dataset will be saved
60-
output_destination: "output/generated_dataset.json"
60+
output_destination: "output/generated_dataset.csv"
Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,86 @@
1-
import logging
2-
3-
from src.config import Config
1+
# configuration params
2+
from langchain_core.language_models import BaseChatModel
3+
from src.writers.abstract_writer import AbstractWriter
4+
from src.search_engine.search_engine_base import BaseSearchEngine
45
from src.utils import parse_args
6+
from src.config import Config
7+
from src.llm.llm_service import LLMService
8+
from src.llm.llm_config import LLMConfig
59

6-
from src.search_engine.solr_search_engine import SolrSearchEngine
10+
# data structures
11+
from src.search_engine.data_store import DataStore
12+
13+
# build factories
14+
from src.llm.llm_provider_factory import LLMServiceFactory
15+
from src.writers.writer_factory import WriterFactory
16+
from src.search_engine.search_engine_factory import SearchEngineFactory
17+
18+
# logging
719
from src.logger import configure_logging
820
import logging
921

22+
1023
if __name__ == "__main__":
1124
args = parse_args()
1225

1326
config = Config.load(args.config_file)
27+
1428
if args.verbose:
1529
configure_logging(logging.DEBUG)
1630
else:
1731
configure_logging(logging.INFO)
32+
log = logging.getLogger(__name__)
33+
34+
search_engine: BaseSearchEngine = SearchEngineFactory.build(search_engine_type=config.search_engine_type,
35+
endpoint=config.search_engine_collection_endpoint)
36+
data_store = DataStore()
37+
38+
num_queries = 0
39+
if config.queries is not None:
40+
with open(config.queries, 'r', encoding='utf-8') as file:
41+
for line in file:
42+
if line.strip():
43+
data_store.add_query(line)
44+
num_queries += 1
45+
46+
# retrieval of the documents needed to generate the queries
47+
docs_to_generate_queries = search_engine.fetch_for_query_generation(documents_filter=config.documents_filter,
48+
doc_number=config.doc_number,
49+
doc_fields=config.doc_fields)
50+
log.debug(f"Number of documents retrieved for generation: {len(docs_to_generate_queries)}")
51+
llm: BaseChatModel = LLMServiceFactory.build(LLMConfig.load(config.llm_configuration_file))
52+
service = LLMService(chat_model=llm)
53+
54+
num_queries_per_doc = int(( (config.num_queries_needed - num_queries) // config.doc_number) * 1.5)
55+
56+
# query generation step
57+
all_queries_generated = False
58+
for doc in docs_to_generate_queries:
59+
data_store.add_document(doc.id, doc)
60+
queries = service.generate_queries(doc, num_queries_per_doc)
61+
for query_text in queries:
62+
if len(data_store.get_queries()) >= config.num_queries_needed:
63+
all_queries_generated = True
64+
break
65+
query_id = data_store.add_query(query_text, doc.id)
66+
data_store.add_rating_score(query_id, doc.id, max(config.relevance_label_set))
67+
if all_queries_generated:
68+
break
69+
70+
log.debug(f"Number of documents evaluated: {len(docs_to_generate_queries)}")
1871

19-
search_engine = SolrSearchEngine('http://localhost:8983/solr/testcore/')
72+
# loop looking at all docs not rated in the data_store for that query
73+
for query_rating_context in data_store.get_queries():
74+
for doc in data_store.get_documents():
75+
if not data_store.has_rating_score(query_rating_context.get_query_id(), doc.id):
76+
score = service.generate_score(data_store.get_document(doc.id),
77+
query_rating_context.get_query(),
78+
config.relevance_scale)
79+
data_store.add_rating_score(query_rating_context.get_query_id(),
80+
doc.id,
81+
score)
2082

21-
docs = search_engine.fetch_for_query_generation(documents_filter=config.documents_filter,
22-
doc_number=config.doc_number,
23-
doc_fields=config.doc_fields)
83+
writer: AbstractWriter = WriterFactory.build(config.output_format, data_store)
84+
writer.write(config.output_destination)
2485

25-
# docs = search_engine.fetch_for_evaluation(keyword="and",
26-
# query_template=config.query_template,
27-
# doc_fields=config.doc_fields)
86+
log.info(f"Synthetic Dataset has been generated in: {config.output_destination}")

rre-dataset-generator/src/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@ def check_config_type(cls, v):
5050
raise ValueError("LLM_config file must have .yaml extension")
5151
return v
5252

53+
@property
54+
def relevance_label_set(self) -> set[int]:
55+
"""
56+
Returns the set of valid labels based on the relevance scale.
57+
"""
58+
if self.relevance_scale == "binary":
59+
return {0, 1}
60+
elif self.relevance_scale == "graded":
61+
return {0, 1, 2}
62+
else:
63+
error_msg = f"Unknown relevance scale: {self.relevance_scale}"
64+
log.error(error_msg)
65+
raise ValueError(error_msg)
66+
5367
@classmethod
5468
def load(cls, config_path: str) -> Config:
5569
"""

rre-dataset-generator/src/llm/llm_provider_factory.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from langchain_openai import ChatOpenAI
1515
from langchain_google_genai import ChatGoogleGenerativeAI
1616
from .llm_config import LLMConfig
17+
from langchain_core.language_models import BaseChatModel
18+
1719

1820
log = logging.getLogger(__name__)
1921

@@ -45,17 +47,17 @@ def build_gemini(config: LLMConfig) -> BaseChatModel:
4547
google_api_key=key,
4648
)
4749

48-
49-
PROVIDER_REGISTRY = {
50-
"openai": build_openai,
51-
"gemini": build_gemini,
52-
}
53-
54-
55-
def build_chat_model(config: LLMConfig) -> BaseChatModel:
56-
provider_name = config.name
57-
if provider_name not in PROVIDER_REGISTRY:
58-
log.error("Unsupported LLM provider requested: %s", provider_name)
59-
raise ValueError(f"Unsupported provider: {provider_name}")
60-
log.info("Selected LLM provider: %s", provider_name)
61-
return PROVIDER_REGISTRY[provider_name](config)
50+
class LLMServiceFactory:
51+
PROVIDER_REGISTRY = {
52+
"openai": build_openai,
53+
"gemini": build_gemini,
54+
}
55+
56+
@classmethod
57+
def build(cls, config: LLMConfig) -> BaseChatModel:
58+
provider_name = config.name
59+
if provider_name not in cls.PROVIDER_REGISTRY:
60+
log.error("Unsupported LLM provider requested: %s", provider_name)
61+
raise ValueError(f"Unsupported provider: {provider_name}")
62+
log.info("Selected LLM provider: %s", provider_name)
63+
return cls.PROVIDER_REGISTRY[provider_name](config)

rre-dataset-generator/src/llm/llm_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from json import JSONDecodeError
2-
from typing import Union, List
2+
from typing import List, Union
33

44
from langchain_core.language_models import BaseChatModel
55
from langchain_core.messages import HumanMessage, SystemMessage
66
import json
77
import logging
88

99
from src.model.document import Document
10+
from typing import Union, List
1011

1112
log = logging.getLogger(__name__)
1213

@@ -22,7 +23,7 @@ def generate_queries(self, document: Document, num_queries_generate_per_doc: int
2223
"""
2324
system_prompt = (
2425
f"You are a helpful assistant! Generate {num_queries_generate_per_doc} "
25-
"keyword-phrase-based queries based on the given document below. "
26+
"queries based on the given document below. "
2627
"**Output only** a JSON array of strings—nothing else. "
2728
"Example format: [\"first query\", \"second query\"]"
2829
)

rre-dataset-generator/src/logger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def configure_logging(level: Union[str, int] = logging.INFO) -> None:
2020
logging.basicConfig(
2121
# filename='logs/log_file_name.log',
2222
level=level,
23-
format='[%(asctime)s] %(filename)-15s:%(lineno)-4d %(levelname)-7s - %(message)s',
23+
format='[%(asctime)s] %(filename)-28s:%(lineno)-4d %(levelname)-7s - %(message)s',
2424
datefmt='%H:%M:%S'
2525
)
2626

rre-dataset-generator/src/model/document.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class Document(BaseModel):
1717
...,
1818
description="Fields of the document."
1919
)
20-
2120
@field_validator('fields')
2221
@classmethod
2322
def check_no_empty_fields(cls, v: Dict[str, Any]) -> Dict[str, Any]:

rre-dataset-generator/src/model/query_rating_context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def add_rating_score(self, doc_id: str, rating_score: int) -> None:
4040
self._doc_id_to_rating_score[doc_id] = rating_score
4141

4242
def has_rating_score(self, doc_id: str) -> bool:
43-
return self._doc_id_to_rating_score[doc_id] != self.DOC_NOT_RATED
43+
return doc_id in self._doc_id_to_rating_score and self._doc_id_to_rating_score[doc_id] != self.DOC_NOT_RATED
4444

4545
def get_rating_score(self, doc_id: str) -> int:
4646
return self._doc_id_to_rating_score[doc_id]

rre-dataset-generator/src/search_engine/data_store.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
from pathlib import Path
66
from typing import Dict, List, Optional
77

8-
from src.logger import configure_logging
98
from src.model.document import Document
109
from src.model.query_rating_context import QueryRatingContext
1110

12-
configure_logging(level=logging.INFO)
1311
log = logging.getLogger(__name__)
1412

1513

@@ -64,7 +62,7 @@ def get_documents(self) -> List[Document]:
6462
"""
6563
return list(self._documents.values())
6664

67-
def add_query(self, query: str, doc_id: str) -> str:
65+
def add_query(self, query: str, doc_id: str | None = None) -> str:
6866
"""
6967
If `query` already exists, just adds `doc_id` to it.
7068
Otherwise, creates a new QueryRatingContext.
@@ -73,7 +71,8 @@ def add_query(self, query: str, doc_id: str) -> str:
7371
if query in self._query_text_to_query_id:
7472
query_id = self._query_text_to_query_id[query]
7573
context = self._queries_by_id[query_id]
76-
context.add_doc_id(doc_id)
74+
if doc_id is not None:
75+
context.add_doc_id(doc_id)
7776
return query_id
7877

7978
# new query rating context

0 commit comments

Comments
 (0)