search-api-webui 0.1.0__tar.gz

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.
@@ -0,0 +1,28 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ venv/
6
+ .env
7
+
8
+ # Frontend / Node
9
+ frontend/node_modules/
10
+ frontend/dist/
11
+ frontend/package-lock.json
12
+ frontend/.DS_Store
13
+ .DS_Store
14
+
15
+ # Backend
16
+ backend/static/
17
+
18
+ # User Configuration
19
+ user_config.json
20
+
21
+ # IDE
22
+ .idea/
23
+ .vscode/
24
+
25
+ # Build artifacts
26
+ build/
27
+ dist/
28
+ *.egg-info/
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 querit-ai
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: search-api-webui
3
+ Version: 0.1.0
4
+ Summary: A Search API WebUI for Querit, You, and other search providers.
5
+ Project-URL: Homepage, https://github.com/querit-ai/search-api-webui
6
+ Project-URL: Repository, https://github.com/querit-ai/search-api-webui.git
7
+ Project-URL: Issues, https://github.com/querit-ai/search-api-webui/issues
8
+ Author: Search API WebUI Team
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,llm,querit,search,tool,webui
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Flask
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.7
17
+ Requires-Dist: flask-cors
18
+ Requires-Dist: flask>=2.0.0
19
+ Requires-Dist: jmespath
20
+ Requires-Dist: pyyaml>=6.0
21
+ Requires-Dist: querit
22
+ Requires-Dist: requests>=2.25.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Search API WebUI
26
+
27
+ A lightweight, local WebUI for testing and visualizing Search APIs (Querit, YDC, etc.).
28
+
29
+ ## Features
30
+
31
+ - 🔍 **Search**: Support for Querit, You.com, and generic Search APIs via configuration.
32
+ - ⚡ **Performance Metrics**: Real-time display of request latency and payload size.
33
+ - 🎨 **Visual Rendering**: Renders standard search results (Title, Snippet, URL) in a clean card layout.
34
+ - 🛠️ **Configurable**: Easy-to-edit `providers.yaml` to add or modify search providers.
35
+ - 🔒 **Secure**: API Keys are stored locally in `user_config.json` and never committed.
36
+
37
+ ## Quick Start
38
+
39
+ ### Prerequisites
40
+
41
+ - Python 3.7+
42
+ - Node.js & npm (for building the frontend)
43
+
44
+ ### Installation
45
+
46
+ 1. **Clone the repository**
47
+ ```bash
48
+ git clone https://github.com/querit-ai/search-api-webui.git
49
+ cd search-api-webui
50
+ ```
51
+ 2. **Build Frontend**
52
+ ```bash
53
+ cd frontend
54
+ npm install
55
+ npm run build
56
+ cd ..
57
+ ```
58
+ 3. **Install Backend**
59
+ ```bash
60
+ pip install -e .
61
+ ```
62
+
63
+ ### Usage
64
+ **Run the server:**
65
+ ```bash
66
+ python -m backend.app
67
+ ```
68
+ Or if you installed via pip:
69
+ ```bash
70
+ search-api-webui
71
+ ```
72
+
73
+ Open your browser at http://localhost:8889.
74
+
75
+ ## Configuration
76
+ ### Add API Keys
77
+ 1. Open the WebUI settings page.
78
+ 2. Enter your API Key for the selected provider (e.g., Querit).
79
+ 3. Keys are saved locally in user_config.json.
80
+
81
+ ### Add New Providers
82
+ Edit providers.yaml to add custom API endpoints. The system uses JMESPath to map JSON responses to the UI.
83
+ ```yaml
84
+ my_custom_search:
85
+ url: "https://api.example.com/search"
86
+ method: "GET"
87
+ ...
88
+ ```
89
+ ## Development
90
+ Backend: Flask (in backend/)
91
+ Frontend: React + Vite (in frontend/)
92
+ ## License
93
+ MIT License. See LICENSE for details.
@@ -0,0 +1,69 @@
1
+ # Search API WebUI
2
+
3
+ A lightweight, local WebUI for testing and visualizing Search APIs (Querit, YDC, etc.).
4
+
5
+ ## Features
6
+
7
+ - 🔍 **Search**: Support for Querit, You.com, and generic Search APIs via configuration.
8
+ - ⚡ **Performance Metrics**: Real-time display of request latency and payload size.
9
+ - 🎨 **Visual Rendering**: Renders standard search results (Title, Snippet, URL) in a clean card layout.
10
+ - 🛠️ **Configurable**: Easy-to-edit `providers.yaml` to add or modify search providers.
11
+ - 🔒 **Secure**: API Keys are stored locally in `user_config.json` and never committed.
12
+
13
+ ## Quick Start
14
+
15
+ ### Prerequisites
16
+
17
+ - Python 3.7+
18
+ - Node.js & npm (for building the frontend)
19
+
20
+ ### Installation
21
+
22
+ 1. **Clone the repository**
23
+ ```bash
24
+ git clone https://github.com/querit-ai/search-api-webui.git
25
+ cd search-api-webui
26
+ ```
27
+ 2. **Build Frontend**
28
+ ```bash
29
+ cd frontend
30
+ npm install
31
+ npm run build
32
+ cd ..
33
+ ```
34
+ 3. **Install Backend**
35
+ ```bash
36
+ pip install -e .
37
+ ```
38
+
39
+ ### Usage
40
+ **Run the server:**
41
+ ```bash
42
+ python -m backend.app
43
+ ```
44
+ Or if you installed via pip:
45
+ ```bash
46
+ search-api-webui
47
+ ```
48
+
49
+ Open your browser at http://localhost:8889.
50
+
51
+ ## Configuration
52
+ ### Add API Keys
53
+ 1. Open the WebUI settings page.
54
+ 2. Enter your API Key for the selected provider (e.g., Querit).
55
+ 3. Keys are saved locally in user_config.json.
56
+
57
+ ### Add New Providers
58
+ Edit providers.yaml to add custom API endpoints. The system uses JMESPath to map JSON responses to the UI.
59
+ ```yaml
60
+ my_custom_search:
61
+ url: "https://api.example.com/search"
62
+ method: "GET"
63
+ ...
64
+ ```
65
+ ## Development
66
+ Backend: Flask (in backend/)
67
+ Frontend: React + Vite (in frontend/)
68
+ ## License
69
+ MIT License. See LICENSE for details.
File without changes
@@ -0,0 +1,146 @@
1
+ import json
2
+ import os
3
+ from flask import Flask, request, jsonify, send_from_directory
4
+ from flask_cors import CORS
5
+ from backend.providers import load_providers
6
+
7
+ app = Flask(__name__, static_folder='static')
8
+ CORS(app)
9
+
10
+ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11
+ PROVIDERS_YAML = os.path.join(BASE_DIR, 'providers.yaml')
12
+ USER_CONFIG_JSON = os.path.join(BASE_DIR, 'user_config.json')
13
+
14
+ provider_map = load_providers(PROVIDERS_YAML)
15
+
16
+ def get_stored_config():
17
+ if not os.path.exists(USER_CONFIG_JSON):
18
+ return {}
19
+ try:
20
+ with open(USER_CONFIG_JSON, 'r') as f:
21
+ return json.load(f)
22
+ except:
23
+ return {}
24
+
25
+ def save_stored_config(config_dict):
26
+ with open(USER_CONFIG_JSON, 'w') as f:
27
+ json.dump(config_dict, f, indent=2)
28
+
29
+ @app.route('/api/providers', methods=['GET'])
30
+ def get_providers_list():
31
+ stored_config = get_stored_config()
32
+ providers_info = []
33
+
34
+ for name, provider_instance in provider_map.items():
35
+ config_details = provider_instance.config
36
+
37
+ user_conf = stored_config.get(name, {})
38
+
39
+ if isinstance(user_conf, str):
40
+ user_conf = {'api_key': user_conf}
41
+
42
+ has_key = bool(user_conf.get('api_key'))
43
+
44
+ providers_info.append({
45
+ "name": name,
46
+ "has_key": has_key,
47
+ "details": config_details,
48
+ "user_settings": {
49
+ "api_url": user_conf.get('api_url', ''),
50
+ "limit": user_conf.get('limit', '10'),
51
+ "language": user_conf.get('language', 'en-US')
52
+ }
53
+ })
54
+ return jsonify(providers_info)
55
+
56
+ @app.route('/api/config', methods=['POST'])
57
+ def update_config():
58
+ data = request.json
59
+ provider_name = data.get('provider')
60
+
61
+ if not provider_name:
62
+ return jsonify({"error": "Provider name is required"}), 400
63
+
64
+ if 'api_key' not in data:
65
+ return jsonify({"error": "API Key field is missing"}), 400
66
+
67
+ api_key = data.get('api_key')
68
+
69
+ api_url = data.get('api_url', '').strip()
70
+ limit = data.get('limit', '10')
71
+ language = data.get('language', 'en-US')
72
+
73
+ all_config = get_stored_config()
74
+
75
+ if provider_name in all_config and isinstance(all_config[provider_name], str):
76
+ all_config[provider_name] = {'api_key': all_config[provider_name]}
77
+
78
+ if not api_key:
79
+ if provider_name in all_config:
80
+ all_config[provider_name]['api_key'] = ""
81
+ else:
82
+ if provider_name not in all_config:
83
+ all_config[provider_name] = {}
84
+
85
+ all_config[provider_name]['api_key'] = api_key
86
+ all_config[provider_name]['api_url'] = api_url
87
+ all_config[provider_name]['limit'] = limit
88
+ all_config[provider_name]['language'] = language
89
+
90
+ save_stored_config(all_config)
91
+ return jsonify({"status": "success"})
92
+
93
+ @app.route('/api/search', methods=['POST'])
94
+ def search_api():
95
+ data = request.json
96
+ query = data.get('query')
97
+ provider_name = data.get('provider', 'querit')
98
+
99
+ api_key = data.get('api_key')
100
+
101
+ stored_config = get_stored_config()
102
+ provider_config = stored_config.get(provider_name, {})
103
+
104
+ if isinstance(provider_config, str):
105
+ provider_config = {'api_key': provider_config}
106
+
107
+ if not api_key:
108
+ api_key = provider_config.get('api_key')
109
+
110
+ if not api_key:
111
+ return jsonify({"error": f"API Key for {provider_name} is missing. Please configure it."}), 401
112
+
113
+ provider = provider_map.get(provider_name)
114
+ if not provider:
115
+ return jsonify({"error": "Provider not found"}), 404
116
+
117
+ search_kwargs = {
118
+ 'api_url': provider_config.get('api_url'),
119
+ 'limit': provider_config.get('limit'),
120
+ 'language': provider_config.get('language')
121
+ }
122
+
123
+ result = provider.search(query, api_key, **search_kwargs)
124
+ return jsonify(result)
125
+
126
+ # Host React Frontend
127
+ @app.route('/', defaults={'path': ''})
128
+ @app.route('/<path:path>')
129
+ def serve(path):
130
+ if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
131
+ return send_from_directory(app.static_folder, path)
132
+ else:
133
+ return send_from_directory(app.static_folder, 'index.html')
134
+
135
+ def main():
136
+ import argparse
137
+ parser = argparse.ArgumentParser(description="Search API WebUI")
138
+ parser.add_argument("--port", type=int, default=8889, help="Port to run the server on")
139
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on")
140
+ args = parser.parse_args()
141
+
142
+ app.run(host=args.host, port=args.port)
143
+
144
+ if __name__ == "__main__":
145
+ main()
146
+
@@ -0,0 +1,34 @@
1
+ import os
2
+ import yaml
3
+ from .generic import GenericProvider
4
+ from .querit import QueritSdkProvider
5
+
6
+ def load_providers(file_path='providers.yaml'):
7
+ """
8
+ Parses the YAML configuration file and instantiates the appropriate provider classes.
9
+
10
+ Args:
11
+ file_path (str): Path to the providers configuration file.
12
+
13
+ Returns:
14
+ dict: A dictionary mapping provider names to their initialized instances.
15
+ """
16
+ if not os.path.exists(file_path):
17
+ print(f"Warning: Provider config file not found at {file_path}")
18
+ return {}
19
+
20
+ with open(file_path, 'r', encoding='utf-8') as f:
21
+ configs = yaml.safe_load(f)
22
+
23
+ providers = {}
24
+ for name, conf in configs.items():
25
+ conf['name'] = name
26
+ provider_type = conf.get('type', 'generic')
27
+
28
+ # Instantiate specific provider based on type or name
29
+ if name == 'querit' or provider_type == 'querit_sdk':
30
+ providers[name] = QueritSdkProvider(conf)
31
+ else:
32
+ providers[name] = GenericProvider(conf)
33
+
34
+ return providers
@@ -0,0 +1,25 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class BaseProvider(ABC):
4
+ """
5
+ Abstract base class for all search providers.
6
+ Enforces a standard interface for executing search queries.
7
+ """
8
+
9
+ @abstractmethod
10
+ def search(self, query, api_key, **kwargs):
11
+ """
12
+ Execute a search request against the provider.
13
+
14
+ Args:
15
+ query (str): The search keywords.
16
+ api_key (str): The API Key required for authentication.
17
+ **kwargs: Arbitrary keyword arguments (e.g., 'limit', 'language', 'api_url').
18
+
19
+ Returns:
20
+ dict: A standardized dictionary containing:
21
+ - 'results': List of dicts with 'title', 'url', 'snippet'.
22
+ - 'metrics': Dict with 'latency_ms' and 'size_bytes'.
23
+ - 'error': (Optional) Error message string if occurred.
24
+ """
25
+ pass
@@ -0,0 +1,125 @@
1
+ import time
2
+ import requests
3
+ import jmespath
4
+ from .base import BaseProvider
5
+
6
+ class GenericProvider(BaseProvider):
7
+ """
8
+ A generic search provider driven by YAML configuration.
9
+ It constructs HTTP requests dynamically and maps responses using JMESPath.
10
+ """
11
+
12
+ def __init__(self, config):
13
+ """
14
+ Initialize the provider with a configuration dictionary.
15
+
16
+ Args:
17
+ config (dict): Configuration containing url, headers, params, and mapping rules.
18
+ """
19
+ self.config = config
20
+
21
+ def _fill_template(self, template_obj, **kwargs):
22
+ """
23
+ Recursively replaces placeholders (e.g., {query}) in dictionaries or strings
24
+ with values provided in kwargs.
25
+
26
+ Args:
27
+ template_obj (dict | str): The structure containing placeholders.
28
+ **kwargs: Key-value pairs to inject into the template.
29
+
30
+ Returns:
31
+ The structure with placeholders replaced by actual values.
32
+ """
33
+ if isinstance(template_obj, str):
34
+ # Treat None values as empty strings to prevent "None" appearing in URLs
35
+ safe_kwargs = {k: (v if v is not None else '') for k, v in kwargs.items()}
36
+ try:
37
+ return template_obj.format(**safe_kwargs)
38
+ except KeyError:
39
+ # Return original string if a placeholder key is missing in kwargs
40
+ return template_obj
41
+ elif isinstance(template_obj, dict):
42
+ return {k: self._fill_template(v, **kwargs) for k, v in template_obj.items()}
43
+ return template_obj
44
+
45
+ def search(self, query, api_key, **kwargs):
46
+ # 1. Extract parameters with defaults
47
+ limit = kwargs.get('limit', '10')
48
+ language = kwargs.get('language', 'en-US')
49
+ custom_url = kwargs.get('api_url', '').strip()
50
+
51
+ # 2. Determine configuration
52
+ url = custom_url if custom_url else self.config.get('url')
53
+ method = self.config.get('method', 'GET')
54
+
55
+ # 3. Prepare context for template injection
56
+ context = {
57
+ 'query': query,
58
+ 'api_key': api_key,
59
+ 'limit': limit,
60
+ 'language': language
61
+ }
62
+
63
+ # 4. construct request components
64
+ headers = self._fill_template(self.config.get('headers', {}), **context)
65
+ params = self._fill_template(self.config.get('params', {}), **context)
66
+ json_body = self._fill_template(self.config.get('payload', {}), **context)
67
+
68
+ # Logging (Masking sensitive API keys)
69
+ print(f'[{self.config.get("name", "Unknown")}] Search:')
70
+ print(f' URL: {url} | Method: {method}')
71
+
72
+ start_time = time.time()
73
+
74
+ try:
75
+ req_args = {'headers': headers, 'timeout': 30}
76
+ if params:
77
+ req_args['params'] = params
78
+ if json_body:
79
+ req_args['json'] = json_body
80
+
81
+ if method.upper() == 'GET':
82
+ response = requests.get(url, **req_args)
83
+ else:
84
+ response = requests.post(url, **req_args)
85
+
86
+ response.raise_for_status()
87
+ except Exception as e:
88
+ print(f"Request Error: {e}")
89
+ return {
90
+ "error": str(e),
91
+ "results": [],
92
+ "metrics": {"latency_ms": 0, "size_bytes": 0}
93
+ }
94
+
95
+ end_time = time.time()
96
+
97
+ # 5. Parse and Normalize Response
98
+ try:
99
+ raw_data = response.json()
100
+ except Exception as e:
101
+ print(f"JSON Parse Error: {e}")
102
+ raw_data = {}
103
+
104
+ mapping = self.config.get('response_mapping', {})
105
+ # Use JMESPath to find the list of results
106
+ root_list = jmespath.search(mapping.get('root_path', '@'), raw_data) or []
107
+
108
+ normalized_results = []
109
+ field_map = mapping.get('fields', {})
110
+
111
+ for item in root_list:
112
+ entry = {}
113
+ # Map specific fields (title, url, etc.) based on config
114
+ for std_key, source_path in field_map.items():
115
+ val = jmespath.search(source_path, item)
116
+ entry[std_key] = val if val else ""
117
+ normalized_results.append(entry)
118
+
119
+ return {
120
+ "results": normalized_results,
121
+ "metrics": {
122
+ "latency_ms": round((end_time - start_time) * 1000, 2),
123
+ "size_bytes": len(response.content)
124
+ }
125
+ }
@@ -0,0 +1,80 @@
1
+ import time
2
+ import json
3
+ from querit import QueritClient
4
+ from querit.models.request import SearchRequest
5
+ from querit.errors import QueritError
6
+ from .base import BaseProvider
7
+
8
+ class QueritSdkProvider(BaseProvider):
9
+ """
10
+ Specialized provider implementation using the official Querit Python SDK.
11
+ """
12
+
13
+ def __init__(self, config):
14
+ self.config = config
15
+
16
+ def search(self, query, api_key, **kwargs):
17
+ """
18
+ Executes a search using the Querit SDK.
19
+ Handles the 'Bearer' prefix logic internally within the SDK.
20
+ """
21
+ try:
22
+ # Initialize client with the raw API key
23
+ client = QueritClient(
24
+ api_key=api_key.strip(),
25
+ timeout=30
26
+ )
27
+
28
+ limit = int(kwargs.get('limit', 10))
29
+
30
+ request_model = SearchRequest(
31
+ query=query,
32
+ count=limit,
33
+ )
34
+
35
+ print(f'[Querit SDK] Searching: {query} (Limit: {limit})')
36
+
37
+ start_time = time.time()
38
+
39
+ # Execute search via SDK
40
+ response = client.search(request_model)
41
+
42
+ end_time = time.time()
43
+
44
+ # Normalize results to standard format
45
+ normalized_results = []
46
+ if response.results:
47
+ for item in response.results:
48
+ # Use getattr to safely access SDK object attributes
49
+ normalized_results.append({
50
+ "title": getattr(item, 'title', ''),
51
+ "url": getattr(item, 'url', ''),
52
+ # Fallback to description if snippet is missing
53
+ "snippet": getattr(item, 'snippet', '') or getattr(item, 'description', '')
54
+ })
55
+
56
+ # Calculate estimated size for metrics (approximate JSON size)
57
+ estimated_size = len(json.dumps([r for r in normalized_results]))
58
+
59
+ return {
60
+ "results": normalized_results,
61
+ "metrics": {
62
+ "latency_ms": round((end_time - start_time) * 1000, 2),
63
+ "size_bytes": estimated_size
64
+ }
65
+ }
66
+
67
+ except QueritError as e:
68
+ print(f"Querit SDK Error: {e}")
69
+ return {
70
+ "error": f"Querit SDK Error: {str(e)}",
71
+ "results": [],
72
+ "metrics": {"latency_ms": 0, "size_bytes": 0}
73
+ }
74
+ except Exception as e:
75
+ print(f"Unexpected Error: {e}")
76
+ return {
77
+ "error": f"Error: {str(e)}",
78
+ "results": [],
79
+ "metrics": {"latency_ms": 0, "size_bytes": 0}
80
+ }