Fix Gradio UI for report generation with detail levels and custom models

This commit is contained in:
Steve White 2025-02-28 10:15:41 -06:00
parent ae130ac49b
commit 0d547d016b
2 changed files with 401 additions and 143 deletions

View File

@ -576,17 +576,19 @@ reranker = get_jina_reranker()
```python
from ranking.jina_reranker import JinaReranker
# Initialize with specific model
reranker = JinaReranker()
query = "What is quantum computing?"
documents = [
"Quantum computing is a computation system that uses quantum mechanics.",
"Classical computers use bits while quantum computers use qubits.",
"Artificial intelligence is transforming various industries."
]
reranked = reranker.rerank(query, documents)
for doc in reranked:
print(f"Score: {doc['score']}, Document: {doc['document']}")
# Rerank documents
results = reranker.rerank(
query="What is quantum computing?",
documents=["Document about quantum physics", "Document about quantum computing", "Document about classical computing"],
top_n=2
)
# Process results
for result in results:
print(f"Score: {result['score']}, Document: {result['document']}")
```
#### Integration with ResultCollector
@ -785,115 +787,141 @@ results = reranker.rerank(
# Process results
for result in results:
print(f"Score: {result['score']}, Document: {result['document']}")
## Query Processor Testing
The query processor module has been tested with the Groq LLM provider to ensure it functions correctly with the newly integrated models.
### Test Scripts
Two test scripts have been created to validate the query processor functionality:
#### Basic Test Script (test_query_processor.py)
```python
# Get the query processor
processor = get_query_processor()
# Process a query
result = processor.process_query("What are the latest advancements in quantum computing?")
# Generate search queries
search_result = processor.generate_search_queries(result, ["google", "bing", "scholar"])
```
- **Purpose**: Tests the core functionality of the query processor
- **Features**:
- Uses monkey patching to ensure the Groq model is used
- Provides detailed output of processing results
## Report Generation Module
#### Comprehensive Test Script (test_query_processor_comprehensive.py)
### ReportDetailLevelManager Class
```python
# Test query enhancement
enhanced_query = test_enhance_query("What is quantum computing?")
# Test query classification
classification = test_classify_query("What is quantum computing?")
# Test the full processing pipeline
structured_query = test_process_query("What is quantum computing?")
# Test search query generation
search_result = test_generate_search_queries(structured_query, ["google", "bing", "scholar"])
```
- **Purpose**: Tests all aspects of the query processor in detail
- **Features**:
- Tests individual components in isolation
- Tests a variety of query types
- Saves detailed test results to a JSON file
## LLM Interface
### LLMInterface Class
The `LLMInterface` class provides a unified interface for interacting with various LLM providers through LiteLLM.
The `ReportDetailLevelManager` class manages configurations for different report detail levels.
#### Initialization
```python
llm = LLMInterface(model_name="gpt-4")
detail_level_manager = get_report_detail_level_manager()
```
- **Description**: Initializes the LLM interface with the specified model
- **Parameters**:
- `model_name` (Optional[str]): The name of the model to use (defaults to config value)
- **Requirements**: Appropriate API key must be set in environment or config
- **Description**: Gets a singleton instance of the ReportDetailLevelManager
#### complete
#### get_detail_level_config
```python
response = llm.complete(prompt, system_prompt=None, temperature=None, max_tokens=None)
config = detail_level_manager.get_detail_level_config(detail_level)
```
- **Description**: Generates a completion for the given prompt
- **Description**: Gets configuration parameters for a specific detail level
- **Parameters**:
- `prompt` (str): The prompt to complete
- `system_prompt` (Optional[str]): System prompt for context
- `temperature` (Optional[float]): Temperature for generation
- `max_tokens` (Optional[int]): Maximum tokens to generate
- **Returns**: str - The generated completion
- **Raises**: LLMError if the completion fails
- `detail_level` (str): Detail level as a string (brief, standard, detailed, comprehensive)
- **Returns**: Dict[str, Any] - Configuration parameters for the specified detail level
- **Raises**: ValueError if the detail level is not valid
#### complete_json
#### get_template_modifier
```python
json_response = llm.complete_json(prompt, system_prompt=None, json_schema=None)
template = detail_level_manager.get_template_modifier(detail_level, query_type)
```
- **Description**: Generates a JSON response for the given prompt
- **Description**: Gets template modifier for a specific detail level and query type
- **Parameters**:
- `prompt` (str): The prompt to complete
- `system_prompt` (Optional[str]): System prompt for context
- `json_schema` (Optional[Dict]): JSON schema for validation
- **Returns**: Dict - The generated JSON response
- **Raises**: LLMError if the completion fails or JSON is invalid
- `detail_level` (str): Detail level as a string (brief, standard, detailed, comprehensive)
- `query_type` (str): Query type as a string (factual, exploratory, comparative)
- **Returns**: str - Template modifier as a string
- **Raises**: ValueError if the detail level or query type is not valid
#### Supported Providers
- OpenAI
- Azure OpenAI
- Anthropic
- Ollama
- Groq
- OpenRouter
#### Example Usage
#### get_available_detail_levels
```python
from query.llm_interface import LLMInterface
levels = detail_level_manager.get_available_detail_levels()
```
- **Description**: Gets a list of available detail levels with descriptions
- **Returns**: List[Tuple[str, str]] - List of tuples containing detail level and description
# Initialize with specific model
llm = LLMInterface(model_name="llama-3.1-8b-instant")
### ReportGenerator Class
# Generate a completion
response = llm.complete(
prompt="Explain quantum computing",
system_prompt="You are a helpful assistant that explains complex topics simply.",
temperature=0.7
The `ReportGenerator` class generates reports from search results.
#### Initialization
```python
report_generator = get_report_generator()
```
- **Description**: Gets a singleton instance of the ReportGenerator
#### initialize
```python
await report_generator.initialize()
```
- **Description**: Initializes the report generator by setting up the database
- **Returns**: None
#### set_detail_level
```python
report_generator.set_detail_level(detail_level)
```
- **Description**: Sets the detail level for report generation
- **Parameters**:
- `detail_level` (str): Detail level (brief, standard, detailed, comprehensive)
- **Returns**: None
- **Raises**: ValueError if the detail level is not valid
#### get_detail_level_config
```python
config = report_generator.get_detail_level_config()
```
- **Description**: Gets the current detail level configuration
- **Returns**: Dict[str, Any] - Configuration parameters for the current detail level
#### get_available_detail_levels
```python
levels = report_generator.get_available_detail_levels()
```
- **Description**: Gets a list of available detail levels with descriptions
- **Returns**: List[Tuple[str, str]] - List of tuples containing detail level and description
#### process_search_results
```python
documents = await report_generator.process_search_results(search_results)
```
- **Description**: Processes search results by scraping the URLs and storing them in the database
- **Parameters**:
- `search_results` (List[Dict[str, Any]]): List of search results, each containing at least a 'url' field
- **Returns**: List[Dict[str, Any]] - List of processed documents
#### prepare_documents_for_report
```python
chunks = await report_generator.prepare_documents_for_report(search_results, token_budget, chunk_size, overlap_size)
```
- **Description**: Prepares documents for report generation by chunking and selecting relevant content
- **Parameters**:
- `search_results` (List[Dict[str, Any]]): List of search results
- `token_budget` (Optional[int]): Maximum number of tokens to use
- `chunk_size` (Optional[int]): Maximum number of tokens per chunk
- `overlap_size` (Optional[int]): Number of tokens to overlap between chunks
- **Returns**: List[Dict[str, Any]] - List of selected document chunks
#### generate_report
```python
report = await report_generator.generate_report(
search_results=search_results,
query=query,
token_budget=token_budget,
chunk_size=chunk_size,
overlap_size=overlap_size,
detail_level=detail_level
)
```
- **Description**: Generates a report from search results
- **Parameters**:
- `search_results` (List[Dict[str, Any]]): List of search results
- `query` (str): Original search query
- `token_budget` (Optional[int]): Maximum number of tokens to use
- `chunk_size` (Optional[int]): Maximum number of tokens per chunk
- `overlap_size` (Optional[int]): Number of tokens to overlap between chunks
- `detail_level` (Optional[str]): Level of detail for the report (brief, standard, detailed, comprehensive)
- **Returns**: str - Generated report as a string
print(response)
#### initialize_report_generator
```python
await initialize_report_generator()
```
- **Description**: Initializes the global report generator instance
- **Returns**: None
#### get_report_generator
```python
report_generator = get_report_generator()
```
- **Description**: Gets the global report generator instance
- **Returns**: ReportGenerator - Initialized report generator instance

View File

@ -8,7 +8,9 @@ import json
import gradio as gr
import sys
import time
import asyncio
from pathlib import Path
from datetime import datetime
# Add the parent directory to the path to allow importing from other modules
sys.path.append(str(Path(__file__).parent.parent))
@ -16,6 +18,9 @@ sys.path.append(str(Path(__file__).parent.parent))
from query.query_processor import QueryProcessor
from execution.search_executor import SearchExecutor
from execution.result_collector import ResultCollector
from report.report_generator import get_report_generator, initialize_report_generator
from report.report_detail_levels import get_report_detail_level_manager, DetailLevel
from config.config import Config
class GradioInterface:
@ -28,6 +33,20 @@ class GradioInterface:
self.result_collector = ResultCollector()
self.results_dir = Path(__file__).parent.parent / "results"
self.results_dir.mkdir(exist_ok=True)
self.reports_dir = Path(__file__).parent.parent
self.reports_dir.mkdir(exist_ok=True)
self.detail_level_manager = get_report_detail_level_manager()
self.config = Config()
# The report generator will be initialized in the async init method
self.report_generator = None
async def async_init(self):
"""Asynchronously initialize components that require async initialization."""
# Initialize the report generator
await initialize_report_generator()
self.report_generator = get_report_generator()
return self
def process_query(self, query, num_results=10, use_reranker=True):
"""
@ -165,6 +184,146 @@ class GradioInterface:
return markdown
async def generate_report(self, query, detail_level, custom_model=None, process_thinking_tags=False, results_file=None):
"""
Generate a report from a query.
Args:
query (str): The query to process
detail_level (str): Detail level for the report
custom_model (str): Custom model to use for report generation
process_thinking_tags (bool): Whether to process thinking tags in the output
results_file (str): Path to results file (optional)
Returns:
tuple: (report_markdown, report_file_path)
"""
try:
# Create a timestamped output file
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
model_suffix = ""
if custom_model:
model_name = custom_model.split('/')[-1]
model_suffix = f"_{model_name}"
output_file = self.reports_dir / f"report_{timestamp}{model_suffix}.md"
# Get detail level configuration
config = self.detail_level_manager.get_detail_level_config(detail_level)
# If custom model is provided, use it
if custom_model:
config["model"] = custom_model
print(f"Generating report with detail level: {detail_level}")
print(f"Detail level configuration: {config}")
print(f"Using model: {config['model']}")
print(f"Processing thinking tags: {process_thinking_tags}")
# If results file is provided, load results from it
search_results = []
if results_file and os.path.exists(results_file):
with open(results_file, 'r') as f:
search_results = json.load(f)
print(f"Loaded {len(search_results)} results from {results_file}")
else:
# If no results file is provided, perform a search
print(f"No results file provided, performing search for: {query}")
# Process the query to create a structured query
structured_query = self.query_processor.process_query(query)
# Generate search queries for different engines
structured_query = self.query_processor.generate_search_queries(
structured_query,
self.search_executor.get_available_search_engines()
)
# Execute the search with the structured query
search_results_dict = self.search_executor.execute_search(
structured_query,
num_results=config["num_results"]
)
# Flatten the search results
search_results = []
for engine_results in search_results_dict.values():
search_results.extend(engine_results)
# Rerank results if we have a reranker
if hasattr(self, 'reranker') and self.reranker:
search_results = self.reranker.rerank_with_metadata(
query,
search_results,
document_key='snippet',
top_n=config["num_results"]
)
# Set the model for report generation if custom model is provided
if custom_model:
# This will update the report synthesizer to use the custom model
self.report_generator.set_detail_level(detail_level)
# Generate the report
report = await self.report_generator.generate_report(
search_results=search_results,
query=query,
token_budget=config["token_budget"],
chunk_size=config["chunk_size"],
overlap_size=config["overlap_size"],
detail_level=detail_level
)
# Process thinking tags if requested
if process_thinking_tags:
report = self._process_thinking_tags(report)
# Save report to file
with open(output_file, 'w', encoding='utf-8') as f:
f.write(report)
print(f"Report saved to: {output_file}")
return report, str(output_file)
except Exception as e:
error_message = f"Error generating report: {str(e)}"
print(f"ERROR: {error_message}")
import traceback
traceback.print_exc()
return f"## Error\n\n{error_message}", None
def _process_thinking_tags(self, text):
"""
Process thinking tags in the text.
Args:
text (str): Text to process
Returns:
str: Processed text
"""
# Remove content between <thinking> and </thinking> tags
import re
return re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL)
def get_available_models(self):
"""
Get a list of available models for report generation.
Returns:
list: List of available model names
"""
# Get models from config
models = [
"llama-3.1-8b-instant",
"llama-3.3-70b-versatile",
"groq/deepseek-r1-distill-llama-70b-specdec",
"openrouter-mixtral",
"openrouter-claude"
]
return models
def create_interface(self):
"""
Create and return the Gradio interface.
@ -179,58 +338,122 @@ class GradioInterface:
This system helps you research topics by searching across multiple sources
including Google (via Serper), Google Scholar, and arXiv.
The system will return ALL results from each search engine, up to the maximum
number specified by the "Results Per Engine" slider. Results are ranked by
relevance across all sources.
You can either search for results or generate a comprehensive report.
"""
)
with gr.Row():
with gr.Column(scale=4):
query_input = gr.Textbox(
label="Research Query",
placeholder="Enter your research question here...",
lines=3
)
with gr.Column(scale=1):
num_results = gr.Slider(
minimum=5,
maximum=50,
value=20,
step=5,
label="Results Per Engine"
)
use_reranker = gr.Checkbox(
label="Use Semantic Reranker",
value=True,
info="Uses Jina AI's reranker for more relevant results"
)
search_button = gr.Button("Search", variant="primary")
with gr.Tabs() as tabs:
with gr.TabItem("Search"):
with gr.Row():
with gr.Column(scale=4):
search_query_input = gr.Textbox(
label="Research Query",
placeholder="Enter your research question here...",
lines=3
)
with gr.Column(scale=1):
search_num_results = gr.Slider(
minimum=5,
maximum=50,
value=20,
step=5,
label="Results Per Engine"
)
search_use_reranker = gr.Checkbox(
label="Use Semantic Reranker",
value=True,
info="Uses Jina AI's reranker for more relevant results"
)
search_button = gr.Button("Search", variant="primary")
gr.Examples(
examples=[
["What are the latest advancements in quantum computing?"],
["Compare transformer and RNN architectures for NLP tasks"],
["Explain the environmental impact of electric vehicles"]
],
inputs=query_input
)
with gr.Row():
with gr.Column():
results_output = gr.Markdown(label="Results")
with gr.Row():
with gr.Column():
file_output = gr.Textbox(
label="Results saved to file",
interactive=False
gr.Examples(
examples=[
["What are the latest advancements in quantum computing?"],
["Compare transformer and RNN architectures for NLP tasks"],
["Explain the environmental impact of electric vehicles"]
],
inputs=search_query_input
)
with gr.Row():
with gr.Column():
search_results_output = gr.Markdown(label="Results")
with gr.Row():
with gr.Column():
search_file_output = gr.Textbox(
label="Results saved to file",
interactive=False
)
with gr.TabItem("Generate Report"):
with gr.Row():
with gr.Column(scale=4):
report_query_input = gr.Textbox(
label="Research Query",
placeholder="Enter your research question here...",
lines=3
)
with gr.Column(scale=1):
report_detail_level = gr.Dropdown(
choices=["brief", "standard", "detailed", "comprehensive"],
value="standard",
label="Detail Level",
info="Controls the depth and breadth of the report"
)
report_custom_model = gr.Dropdown(
choices=self.get_available_models(),
value=None,
label="Custom Model (Optional)",
info="Select a custom model for report generation"
)
report_process_thinking = gr.Checkbox(
label="Process Thinking Tags",
value=False,
info="Process <thinking> tags in model output"
)
report_button = gr.Button("Generate Report", variant="primary")
gr.Examples(
examples=[
["What are the latest advancements in quantum computing?"],
["Compare transformer and RNN architectures for NLP tasks"],
["Explain the environmental impact of electric vehicles"],
["Explain the potential relationship between creatine supplementation and muscle loss due to GLP1-ar drugs for weight loss."]
],
inputs=report_query_input
)
with gr.Row():
with gr.Column():
report_output = gr.Markdown(label="Generated Report")
with gr.Row():
with gr.Column():
report_file_output = gr.Textbox(
label="Report saved to file",
interactive=False
)
# Add information about detail levels
detail_levels_info = ""
for level, description in self.detail_level_manager.get_available_detail_levels():
detail_levels_info += f"- **{level}**: {description}\n"
gr.Markdown(f"### Detail Levels\n{detail_levels_info}")
# Set up event handlers
search_button.click(
fn=self.process_query,
inputs=[query_input, num_results, use_reranker],
outputs=[results_output, file_output]
inputs=[search_query_input, search_num_results, search_use_reranker],
outputs=[search_results_output, search_file_output]
)
report_button.click(
fn=lambda q, d, m, p, f: asyncio.run(self.generate_report(q, d, m, p, f)),
inputs=[report_query_input, report_detail_level, report_custom_model,
report_process_thinking, search_file_output],
outputs=[report_output, report_file_output]
)
return interface
@ -248,7 +471,14 @@ class GradioInterface:
def main():
"""Main function to launch the Gradio interface."""
# Create interface and initialize async components
interface = GradioInterface()
# Run the async initialization in the event loop
loop = asyncio.get_event_loop()
loop.run_until_complete(interface.async_init())
# Launch the interface
interface.launch(share=True)