Skip to content

Commit f03501f

Browse files
author
sun
committed
refactor(datasetgen): split generator module into focused components
fix: resolve taxonomy dataset generation issues and enhance system initialization
1 parent 5352ee8 commit f03501f

15 files changed

Lines changed: 1951 additions & 2322 deletions

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ COMPILEO_API_KEYS=default-api-key
1919
# REDIS CONFIGURATION
2020
# ==========================================
2121
# Redis connection URL (required for job queuing)
22-
REDIS_URL=redis://redis:6379/0
22+
# For local Python development: use localhost
23+
# For Docker deployment: use redis container name (set by docker-compose.yml)
24+
# If not set, defaults to redis://localhost:6379/0 for local development
25+
# REDIS_URL=redis://localhost:6379/0
2326

2427
# ==========================================
2528
# API ENDPOINT CONFIGURATION

requirements-docker.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pikepdf
3232
opencv-python==4.12.0.88
3333
pdfminer.six==20250506
3434
pypdf==6.0.0
35-
pdf2image
35+
3636
PyMuPDF==1.26.4
3737
unstructured[docx,pptx,xlsx,md]==0.18.15
3838
unstructured-client==0.42.3

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pikepdf
3131
opencv-python==4.12.0.88
3232
pdfminer.six==20250506
3333
pypdf==6.0.0
34-
pdf2image
34+
3535
PyMuPDF==1.26.4
3636
unstructured[docx,pptx,xlsx,md]==0.18.15
3737
unstructured-client==0.42.3

src/compileo/features/datasetgen/data_loader.py

Lines changed: 590 additions & 0 deletions
Large diffs are not rendered by default.

src/compileo/features/datasetgen/generator.py

Lines changed: 260 additions & 2308 deletions
Large diffs are not rendered by default.

src/compileo/features/datasetgen/prompt_builder.py

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ def build_prompt(self, project_id: int, prompt_name: str, chunk_text: Optional[s
3939
prompt_record = self.prompt_repository.get_by_name(prompt_name)
4040
if prompt_record:
4141
prompt_template = prompt_record[2] # content is the third column (index 2)
42-
elif prompt_name == "default":
43-
# Hardcoded default prompt template for dataset generation
42+
elif prompt_name == "default" or prompt_name == "qa_pairs":
43+
# Hardcoded default prompt template for Q&A dataset generation
4444
prompt_template = """Generate {datasets_per_chunk} question-answer pairs from the following content.
4545
4646
Content: {chunk}
@@ -52,6 +52,8 @@ def build_prompt(self, project_id: int, prompt_name: str, chunk_text: Optional[s
5252
- Vary question types (factual, analytical, applicative)
5353
- Focus on key concepts, important details, and relationships in the content
5454
55+
{taxonomy_instructions}
56+
5557
{purpose_context}{audience_context}{complexity_level_context}
5658
5759
Format your response as a JSON array of objects with this structure:
@@ -84,6 +86,12 @@ def build_prompt(self, project_id: int, prompt_name: str, chunk_text: Optional[s
8486
# Ensure datasets_per_chunk is in param_dict
8587
param_dict['datasets_per_chunk'] = datasets_per_chunk
8688

89+
# Ensure taxonomy-related parameters exist
90+
param_dict.setdefault('taxonomy_instructions', '')
91+
param_dict.setdefault('taxonomy_name', '')
92+
param_dict.setdefault('taxonomy_description', '')
93+
param_dict.setdefault('taxonomy_categories', '')
94+
8795
# Load taxonomy if specified
8896
taxonomy_info = None
8997
if taxonomy_project and taxonomy_name:
@@ -108,6 +116,9 @@ def build_prompt(self, project_id: int, prompt_name: str, chunk_text: Optional[s
108116
if taxonomy_info:
109117
param_dict.update(taxonomy_info)
110118

119+
# Handle legacy {Na} placeholder (maps to taxonomy_name)
120+
param_dict.setdefault('Na', param_dict.get('taxonomy_name', ''))
121+
111122
# Replace {chunk} with actual chunk text if provided
112123
if chunk_text is not None:
113124
if "{chunk}" in prompt_template:
@@ -244,12 +255,39 @@ def build_question_generation_prompt(self, project_id: int, chunk_text: str,
244255
except Exception as e:
245256
logger.warning(f"Could not load taxonomy {taxonomy_project}/{taxonomy_name}: {e}")
246257

258+
# Ensure taxonomy-related parameters exist even when no taxonomy is loaded
259+
param_dict.setdefault('taxonomy_instructions', '')
260+
param_dict.setdefault('taxonomy_name', '')
261+
param_dict.setdefault('taxonomy_description', '')
262+
param_dict.setdefault('taxonomy_categories', '')
263+
247264
prompt_record = self.prompt_repository.get_by_name("question_generation")
248265
if prompt_record:
249266
prompt_template = prompt_record[2]
250267
else:
251268
# Fallback template
252-
prompt_template = "Generate a question based on the following content: {chunk_text}"
269+
prompt_template = """Generate {datasets_per_chunk} questions from the following content.
270+
271+
Content: {chunk_text}
272+
273+
Instructions:
274+
- Create diverse, high-quality questions that test understanding of the content
275+
- Ensure questions are answerable using only the provided information
276+
- Vary question types (factual, analytical, applicative)
277+
- Focus on key concepts, important details, and relationships in the content
278+
279+
{taxonomy_instructions}
280+
281+
{purpose_context}{audience_context}{complexity_level_context}
282+
283+
Format your response as a JSON array of objects with this structure:
284+
[
285+
{{
286+
"question": "Your question here",
287+
"category": "content_category",
288+
"difficulty": "easy|medium|hard"
289+
}}
290+
]"""
253291

254292
# Format the prompt with the parameters and the chunk text
255293
final_prompt = prompt_template.format(
@@ -319,12 +357,39 @@ def build_answer_generation_prompt(self, project_id: int, chunk_text: str,
319357
except Exception as e:
320358
logger.warning(f"Could not load taxonomy {taxonomy_project}/{taxonomy_name}: {e}")
321359

360+
# Ensure taxonomy-related parameters exist even when no taxonomy is loaded
361+
param_dict.setdefault('taxonomy_instructions', '')
362+
param_dict.setdefault('taxonomy_name', '')
363+
param_dict.setdefault('taxonomy_description', '')
364+
param_dict.setdefault('taxonomy_categories', '')
365+
322366
prompt_record = self.prompt_repository.get_by_name("answer_generation")
323367
if prompt_record:
324368
prompt_template = prompt_record[2]
325369
else:
326370
# Fallback template
327-
prompt_template = "Generate an answer for the following content: {chunk_text}"
371+
prompt_template = """Generate {datasets_per_chunk} answers for the following question based on the content.
372+
373+
Question: {question}
374+
Content: {chunk_text}
375+
376+
Instructions:
377+
- Provide accurate, comprehensive answers based only on the given content
378+
- Ensure answers are supported by the provided information
379+
- Include relevant details and context from the content
380+
381+
{taxonomy_instructions}
382+
383+
{purpose_context}{audience_context}{complexity_level_context}
384+
385+
Format your response as a JSON array of objects with this structure:
386+
[
387+
{{
388+
"answer": "Your answer here",
389+
"category": "content_category",
390+
"confidence": 0.0-1.0
391+
}}
392+
]"""
328393

329394
# Format the prompt with the parameters and the chunk text
330395
final_prompt = prompt_template.format(
@@ -394,12 +459,38 @@ def build_summarization_prompt(self, project_id: int, chunk_text: str,
394459
except Exception as e:
395460
logger.warning(f"Could not load taxonomy {taxonomy_project}/{taxonomy_name}: {e}")
396461

462+
# Ensure taxonomy-related parameters exist even when no taxonomy is loaded
463+
param_dict.setdefault('taxonomy_instructions', '')
464+
param_dict.setdefault('taxonomy_name', '')
465+
param_dict.setdefault('taxonomy_description', '')
466+
param_dict.setdefault('taxonomy_categories', '')
467+
397468
prompt_record = self.prompt_repository.get_by_name("summarization")
398469
if prompt_record:
399470
prompt_template = prompt_record[2]
400471
else:
401472
# Fallback template
402-
prompt_template = "Summarize the following content in a concise manner: {chunk_text}"
473+
prompt_template = """Generate {datasets_per_chunk} summaries from the following content.
474+
475+
Content: {chunk_text}
476+
477+
Instructions:
478+
- Create concise, accurate summaries that capture the main points
479+
- Focus on key concepts and important details
480+
- Maintain factual accuracy from the source content
481+
482+
{taxonomy_instructions}
483+
484+
{purpose_context}{audience_context}{complexity_level_context}
485+
486+
Format your response as a JSON array of objects with this structure:
487+
[
488+
{{
489+
"summary": "Your summary here",
490+
"key_points": ["Point 1", "Point 2", "Point 3"],
491+
"category": "content_category"
492+
}}
493+
]"""
403494

404495
# Format the prompt with the parameters and the chunk text
405496
final_prompt = prompt_template.format(

src/compileo/features/datasetgen/strategies/__init__.py

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from typing import Any, Dict, List, Optional
2+
from ..prompt_builder import PromptBuilder
3+
from ..llm_interaction import LLMInteraction
4+
5+
class BaseGenerationStrategy:
6+
"""
7+
Base class for dataset generation strategies.
8+
"""
9+
def __init__(self, prompt_builder: PromptBuilder, llm_interaction: LLMInteraction):
10+
self.prompt_builder = prompt_builder
11+
self.llm_interaction = llm_interaction
12+
13+
def generate(self, *args, **kwargs) -> List[Dict[str, Any]]:
14+
raise NotImplementedError("Subclasses must implement generate method.")
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from typing import List, Dict, Any, Optional
2+
from src.compileo.core.logging import get_logger
3+
from .base_strategy import BaseGenerationStrategy
4+
5+
logger = get_logger(__name__)
6+
7+
class ChunkGenerationStrategy(BaseGenerationStrategy):
8+
"""
9+
Strategy for generating datasets from raw text chunks.
10+
"""
11+
12+
def generate_qa(self, project_id: int, prompt_name: str, chunk: str,
13+
taxonomy_project: Optional[str] = None, taxonomy_name: Optional[str] = None,
14+
datasets_per_chunk: int = 3) -> List[Dict[str, Any]]:
15+
"""
16+
Generates multiple question-answer pairs for a single chunk.
17+
"""
18+
results = []
19+
for i in range(datasets_per_chunk):
20+
final_prompt = self.prompt_builder.build_prompt(project_id, prompt_name, chunk,
21+
taxonomy_project=taxonomy_project, taxonomy_name=taxonomy_name,
22+
datasets_per_chunk=datasets_per_chunk)
23+
# Modify prompt to request multiple items if needed
24+
if datasets_per_chunk > 1:
25+
final_prompt += f"\n\nGenerate {datasets_per_chunk} different question-answer pairs from this text chunk. Return them as a JSON array."
26+
27+
result = self.llm_interaction.generate(final_prompt)
28+
29+
# Handle multiple results
30+
if isinstance(result, list):
31+
for item in result[:datasets_per_chunk]:
32+
results.append(item)
33+
else:
34+
results.append(result)
35+
36+
if len(results) >= datasets_per_chunk:
37+
break
38+
39+
return results[:datasets_per_chunk]
40+
41+
def generate_questions(self, project_id: int, chunk: str,
42+
taxonomy_project: Optional[str] = None, taxonomy_name: Optional[str] = None,
43+
datasets_per_chunk: int = 3) -> List[Dict[str, Any]]:
44+
"""
45+
Generates multiple questions for a single chunk.
46+
"""
47+
results = []
48+
for i in range(datasets_per_chunk):
49+
final_prompt = self.prompt_builder.build_question_generation_prompt(project_id, chunk, taxonomy_project, taxonomy_name)
50+
# Modify prompt to request multiple items if needed
51+
if datasets_per_chunk > 1:
52+
final_prompt += f"\n\nGenerate {datasets_per_chunk} different questions from this text chunk. Return them as a JSON array."
53+
54+
result = self.llm_interaction.generate(final_prompt)
55+
56+
# Handle multiple results
57+
if isinstance(result, list):
58+
for item in result[:datasets_per_chunk]:
59+
item["result_type"] = "questions"
60+
results.append(item)
61+
else:
62+
result["result_type"] = "questions"
63+
results.append(result)
64+
65+
if len(results) >= datasets_per_chunk:
66+
break
67+
68+
return results[:datasets_per_chunk]
69+
70+
def generate_answers(self, project_id: int, chunk: str,
71+
taxonomy_project: Optional[str] = None, taxonomy_name: Optional[str] = None,
72+
datasets_per_chunk: int = 3) -> List[Dict[str, Any]]:
73+
"""
74+
Generates multiple answers for a single chunk.
75+
"""
76+
results = []
77+
for i in range(datasets_per_chunk):
78+
final_prompt = self.prompt_builder.build_answer_generation_prompt(project_id, chunk, taxonomy_project, taxonomy_name)
79+
# Modify prompt to request multiple items if needed
80+
if datasets_per_chunk > 1:
81+
final_prompt += f"\n\nGenerate {datasets_per_chunk} different answers from this text chunk. Return them as a JSON array."
82+
83+
result = self.llm_interaction.generate(final_prompt)
84+
85+
# Handle multiple results
86+
if isinstance(result, list):
87+
for item in result[:datasets_per_chunk]:
88+
item["result_type"] = "answers"
89+
results.append(item)
90+
else:
91+
result["result_type"] = "answers"
92+
results.append(result)
93+
94+
if len(results) >= datasets_per_chunk:
95+
break
96+
97+
return results[:datasets_per_chunk]
98+
99+
def generate_summaries(self, project_id: int, chunk: str,
100+
taxonomy_project: Optional[str] = None, taxonomy_name: Optional[str] = None,
101+
datasets_per_chunk: int = 3) -> List[Dict[str, Any]]:
102+
"""
103+
Generates multiple summaries for a single chunk.
104+
"""
105+
results = []
106+
for i in range(datasets_per_chunk):
107+
final_prompt = self.prompt_builder.build_summarization_prompt(project_id, chunk, taxonomy_project, taxonomy_name)
108+
# Modify prompt to request multiple items if needed
109+
if datasets_per_chunk > 1:
110+
final_prompt += f"\n\nGenerate {datasets_per_chunk} different summaries from this text chunk. Return them as a JSON array."
111+
112+
result = self.llm_interaction.generate(final_prompt)
113+
114+
# Handle multiple results
115+
if isinstance(result, list):
116+
for item in result[:datasets_per_chunk]:
117+
item["result_type"] = "summary"
118+
results.append(item)
119+
else:
120+
result["result_type"] = "summary"
121+
results.append(result)
122+
123+
if len(results) >= datasets_per_chunk:
124+
break
125+
126+
return results[:datasets_per_chunk]

0 commit comments

Comments
 (0)