universal-mcp 0.1.23rc2__py3-none-any.whl → 0.1.24rc2__py3-none-any.whl
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.
- universal_mcp/analytics.py +43 -11
- universal_mcp/applications/application.py +186 -132
- universal_mcp/applications/sample_tool_app.py +80 -0
- universal_mcp/cli.py +5 -229
- universal_mcp/client/agents/__init__.py +4 -0
- universal_mcp/client/agents/base.py +38 -0
- universal_mcp/client/agents/llm.py +115 -0
- universal_mcp/client/agents/react.py +67 -0
- universal_mcp/client/cli.py +181 -0
- universal_mcp/client/oauth.py +122 -18
- universal_mcp/client/token_store.py +62 -3
- universal_mcp/client/{client.py → transport.py} +127 -48
- universal_mcp/config.py +160 -46
- universal_mcp/exceptions.py +50 -6
- universal_mcp/integrations/__init__.py +1 -4
- universal_mcp/integrations/integration.py +220 -121
- universal_mcp/servers/__init__.py +1 -1
- universal_mcp/servers/server.py +114 -247
- universal_mcp/stores/store.py +126 -93
- universal_mcp/tools/func_metadata.py +1 -1
- universal_mcp/tools/manager.py +15 -3
- universal_mcp/tools/tools.py +2 -2
- universal_mcp/utils/agentr.py +3 -4
- universal_mcp/utils/installation.py +3 -4
- universal_mcp/utils/openapi/api_generator.py +28 -2
- universal_mcp/utils/openapi/api_splitter.py +0 -1
- universal_mcp/utils/openapi/cli.py +243 -0
- universal_mcp/utils/openapi/filters.py +114 -0
- universal_mcp/utils/openapi/openapi.py +31 -2
- universal_mcp/utils/openapi/preprocessor.py +62 -7
- universal_mcp/utils/prompts.py +787 -0
- universal_mcp/utils/singleton.py +4 -1
- universal_mcp/utils/testing.py +6 -6
- universal_mcp-0.1.24rc2.dist-info/METADATA +54 -0
- universal_mcp-0.1.24rc2.dist-info/RECORD +53 -0
- universal_mcp/applications/README.md +0 -122
- universal_mcp/client/__main__.py +0 -30
- universal_mcp/client/agent.py +0 -96
- universal_mcp/integrations/README.md +0 -25
- universal_mcp/servers/README.md +0 -79
- universal_mcp/stores/README.md +0 -74
- universal_mcp/tools/README.md +0 -86
- universal_mcp-0.1.23rc2.dist-info/METADATA +0 -283
- universal_mcp-0.1.23rc2.dist-info/RECORD +0 -51
- /universal_mcp/{utils → tools}/docstring_parser.py +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/WHEEL +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/entry_points.txt +0 -0
- {universal_mcp-0.1.23rc2.dist-info → universal_mcp-0.1.24rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import typer
|
5
|
+
from rich.console import Console
|
6
|
+
|
7
|
+
# Setup rich console and logging
|
8
|
+
console = Console()
|
9
|
+
|
10
|
+
app = typer.Typer(name="codegen")
|
11
|
+
|
12
|
+
|
13
|
+
@app.command()
|
14
|
+
def generate(
|
15
|
+
schema_path: Path = typer.Option(..., "--schema", "-s"),
|
16
|
+
output_path: Path = typer.Option(
|
17
|
+
None,
|
18
|
+
"--output",
|
19
|
+
"-o",
|
20
|
+
help="Output file path - should match the API name (e.g., 'twitter.py' for Twitter API)",
|
21
|
+
),
|
22
|
+
class_name: str = typer.Option(
|
23
|
+
None,
|
24
|
+
"--class-name",
|
25
|
+
"-c",
|
26
|
+
help="Class name to use for the API client",
|
27
|
+
),
|
28
|
+
):
|
29
|
+
"""Generate API client from OpenAPI schema with optional docstring generation.
|
30
|
+
|
31
|
+
The output filename should match the name of the API in the schema (e.g., 'twitter.py' for Twitter API).
|
32
|
+
This name will be used for the folder in applications/.
|
33
|
+
"""
|
34
|
+
# Import here to avoid circular imports
|
35
|
+
from universal_mcp.utils.openapi.api_generator import generate_api_from_schema
|
36
|
+
|
37
|
+
if not schema_path.exists():
|
38
|
+
console.print(f"[red]Error: Schema file {schema_path} does not exist[/red]")
|
39
|
+
raise typer.Exit(1)
|
40
|
+
|
41
|
+
try:
|
42
|
+
app_file_data = generate_api_from_schema(
|
43
|
+
schema_path=schema_path,
|
44
|
+
output_path=output_path,
|
45
|
+
class_name=class_name,
|
46
|
+
)
|
47
|
+
if isinstance(app_file_data, dict) and "code" in app_file_data:
|
48
|
+
console.print("[yellow]No output path specified, printing generated code to console:[/yellow]")
|
49
|
+
console.print(app_file_data["code"])
|
50
|
+
elif isinstance(app_file_data, Path):
|
51
|
+
console.print("[green]API client successfully generated and installed.[/green]")
|
52
|
+
console.print(f"[blue]Application file: {app_file_data}[/blue]")
|
53
|
+
else:
|
54
|
+
# Handle the error case from api_generator if validation fails
|
55
|
+
if isinstance(app_file_data, dict) and "error" in app_file_data:
|
56
|
+
console.print(f"[red]{app_file_data['error']}[/red]")
|
57
|
+
raise typer.Exit(1)
|
58
|
+
else:
|
59
|
+
console.print("[red]Unexpected return value from API generator.[/red]")
|
60
|
+
raise typer.Exit(1)
|
61
|
+
|
62
|
+
except Exception as e:
|
63
|
+
console.print(f"[red]Error generating API client: {e}[/red]")
|
64
|
+
raise typer.Exit(1) from e
|
65
|
+
|
66
|
+
|
67
|
+
@app.command()
|
68
|
+
def readme(
|
69
|
+
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
70
|
+
):
|
71
|
+
"""Generate a README.md file for the API client."""
|
72
|
+
from universal_mcp.utils.openapi.readme import generate_readme
|
73
|
+
|
74
|
+
readme_file = generate_readme(file_path)
|
75
|
+
console.print(f"[green]README.md file generated at: {readme_file}[/green]")
|
76
|
+
|
77
|
+
|
78
|
+
@app.command()
|
79
|
+
def docgen(
|
80
|
+
file_path: Path = typer.Argument(..., help="Path to the Python file to process"),
|
81
|
+
model: str = typer.Option(
|
82
|
+
"perplexity/sonar",
|
83
|
+
"--model",
|
84
|
+
"-m",
|
85
|
+
help="Model to use for generating docstrings",
|
86
|
+
),
|
87
|
+
):
|
88
|
+
"""Generate docstrings for Python files using LLMs.
|
89
|
+
|
90
|
+
This command uses litellm with structured output to generate high-quality
|
91
|
+
Google-style docstrings for all functions in the specified Python file.
|
92
|
+
"""
|
93
|
+
from universal_mcp.utils.openapi.docgen import process_file
|
94
|
+
|
95
|
+
if not file_path.exists():
|
96
|
+
console.print(f"[red]Error: File not found: {file_path}[/red]")
|
97
|
+
raise typer.Exit(1)
|
98
|
+
|
99
|
+
try:
|
100
|
+
processed = process_file(str(file_path), model)
|
101
|
+
console.print(f"[green]Successfully processed {processed} functions[/green]")
|
102
|
+
except Exception as e:
|
103
|
+
console.print(f"[red]Error: {e}[/red]")
|
104
|
+
raise typer.Exit(1) from e
|
105
|
+
|
106
|
+
|
107
|
+
@app.command()
|
108
|
+
def init(
|
109
|
+
output_dir: Path | None = typer.Option(
|
110
|
+
None,
|
111
|
+
"--output-dir",
|
112
|
+
"-o",
|
113
|
+
help="Output directory for the project (must exist)",
|
114
|
+
),
|
115
|
+
app_name: str | None = typer.Option(
|
116
|
+
None,
|
117
|
+
"--app-name",
|
118
|
+
"-a",
|
119
|
+
help="App name (letters, numbers, hyphens, underscores only)",
|
120
|
+
),
|
121
|
+
integration_type: str | None = typer.Option(
|
122
|
+
None,
|
123
|
+
"--integration-type",
|
124
|
+
"-i",
|
125
|
+
help="Integration type (api_key, oauth, agentr, none)",
|
126
|
+
case_sensitive=False,
|
127
|
+
show_choices=True,
|
128
|
+
),
|
129
|
+
):
|
130
|
+
"""Initialize a new MCP project using the cookiecutter template."""
|
131
|
+
from cookiecutter.main import cookiecutter
|
132
|
+
|
133
|
+
NAME_PATTERN = r"^[a-zA-Z0-9_-]+$"
|
134
|
+
|
135
|
+
def validate_pattern(value: str, field_name: str) -> None:
|
136
|
+
if not re.match(NAME_PATTERN, value):
|
137
|
+
console.print(
|
138
|
+
f"[red]❌ Invalid {field_name}; only letters, numbers, hyphens, and underscores allowed.[/red]"
|
139
|
+
)
|
140
|
+
raise typer.Exit(code=1)
|
141
|
+
|
142
|
+
# App name
|
143
|
+
if not app_name:
|
144
|
+
app_name = typer.prompt(
|
145
|
+
"Enter the app name",
|
146
|
+
default="app_name",
|
147
|
+
prompt_suffix=" (e.g., reddit, youtube): ",
|
148
|
+
).strip()
|
149
|
+
validate_pattern(app_name, "app name")
|
150
|
+
app_name = app_name.lower()
|
151
|
+
if not output_dir:
|
152
|
+
path_str = typer.prompt(
|
153
|
+
"Enter the output directory for the project",
|
154
|
+
default=str(Path.cwd()),
|
155
|
+
prompt_suffix=": ",
|
156
|
+
).strip()
|
157
|
+
output_dir = Path(path_str)
|
158
|
+
|
159
|
+
if not output_dir.exists():
|
160
|
+
try:
|
161
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
162
|
+
console.print(f"[green]✅ Created output directory at '{output_dir}'[/green]")
|
163
|
+
except Exception as e:
|
164
|
+
console.print(f"[red]❌ Failed to create output directory '{output_dir}': {e}[/red]")
|
165
|
+
raise typer.Exit(code=1) from e
|
166
|
+
elif not output_dir.is_dir():
|
167
|
+
console.print(f"[red]❌ Output path '{output_dir}' exists but is not a directory.[/red]")
|
168
|
+
raise typer.Exit(code=1)
|
169
|
+
|
170
|
+
# Integration type
|
171
|
+
if not integration_type:
|
172
|
+
integration_type = typer.prompt(
|
173
|
+
"Choose the integration type",
|
174
|
+
default="agentr",
|
175
|
+
prompt_suffix=" (api_key, oauth, agentr, none): ",
|
176
|
+
).lower()
|
177
|
+
if integration_type not in ("api_key", "oauth", "agentr", "none"):
|
178
|
+
console.print("[red]❌ Integration type must be one of: api_key, oauth, agentr, none[/red]")
|
179
|
+
raise typer.Exit(code=1)
|
180
|
+
|
181
|
+
console.print("[blue]🚀 Generating project using cookiecutter...[/blue]")
|
182
|
+
try:
|
183
|
+
cookiecutter(
|
184
|
+
"https://github.com/AgentrDev/universal-mcp-app-template.git",
|
185
|
+
output_dir=str(output_dir),
|
186
|
+
no_input=True,
|
187
|
+
extra_context={
|
188
|
+
"app_name": app_name,
|
189
|
+
"integration_type": integration_type,
|
190
|
+
},
|
191
|
+
)
|
192
|
+
except Exception as exc:
|
193
|
+
console.print(f"❌ Project generation failed: {exc}")
|
194
|
+
raise typer.Exit(code=1) from exc
|
195
|
+
|
196
|
+
project_dir = output_dir / f"{app_name}"
|
197
|
+
console.print(f"✅ Project created at {project_dir}")
|
198
|
+
|
199
|
+
|
200
|
+
@app.command()
|
201
|
+
def preprocess(
|
202
|
+
schema_path: Path = typer.Option(None, "--schema", "-s", help="Path to the OpenAPI schema file."),
|
203
|
+
output_path: Path = typer.Option(None, "--output", "-o", help="Path to save the processed schema."),
|
204
|
+
):
|
205
|
+
from universal_mcp.utils.openapi.preprocessor import run_preprocessing
|
206
|
+
|
207
|
+
"""Preprocess an OpenAPI schema using LLM to fill or enhance descriptions."""
|
208
|
+
run_preprocessing(schema_path, output_path)
|
209
|
+
|
210
|
+
|
211
|
+
@app.command()
|
212
|
+
def split_api(
|
213
|
+
input_app_file: Path = typer.Argument(..., help="Path to the generated app.py file to split"),
|
214
|
+
output_dir: Path = typer.Option(..., "--output-dir", "-o", help="Directory to save the split files"),
|
215
|
+
package_name: str = typer.Option(
|
216
|
+
None, "--package-name", "-p", help="Package name for absolute imports (e.g., 'hubspot')"
|
217
|
+
),
|
218
|
+
):
|
219
|
+
"""Splits a single generated API client file into multiple files based on path groups."""
|
220
|
+
from universal_mcp.utils.openapi.api_splitter import split_generated_app_file
|
221
|
+
|
222
|
+
if not input_app_file.exists() or not input_app_file.is_file():
|
223
|
+
console.print(f"[red]Error: Input file {input_app_file} does not exist or is not a file.[/red]")
|
224
|
+
raise typer.Exit(1)
|
225
|
+
|
226
|
+
if not output_dir.exists():
|
227
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
228
|
+
console.print(f"[green]Created output directory: {output_dir}[/green]")
|
229
|
+
elif not output_dir.is_dir():
|
230
|
+
console.print(f"[red]Error: Output path {output_dir} is not a directory.[/red]")
|
231
|
+
raise typer.Exit(1)
|
232
|
+
|
233
|
+
try:
|
234
|
+
split_generated_app_file(input_app_file, output_dir, package_name)
|
235
|
+
console.print(f"[green]Successfully split {input_app_file} into {output_dir}[/green]")
|
236
|
+
except Exception as e:
|
237
|
+
console.print(f"[red]Error splitting API client: {e}[/red]")
|
238
|
+
|
239
|
+
raise typer.Exit(1) from e
|
240
|
+
|
241
|
+
|
242
|
+
if __name__ == "__main__":
|
243
|
+
app()
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"""
|
2
|
+
Shared filtering utilities for OpenAPI selective processing.
|
3
|
+
|
4
|
+
This module contains common functions used by both the preprocessor
|
5
|
+
and API client generator for filtering operations based on JSON configuration.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
import os
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
def load_filter_config(config_path: str) -> dict[str, str | list[str]]:
|
16
|
+
"""
|
17
|
+
Load the JSON filter configuration file for selective processing.
|
18
|
+
|
19
|
+
Expected format:
|
20
|
+
{
|
21
|
+
"/users/{user-id}/profile": "get",
|
22
|
+
"/users/{user-id}/settings": "all",
|
23
|
+
"/orders/{order-id}": ["get", "put", "delete"]
|
24
|
+
}
|
25
|
+
|
26
|
+
Args:
|
27
|
+
config_path: Path to the JSON configuration file
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
Dictionary mapping paths to methods
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
FileNotFoundError: If config file doesn't exist
|
34
|
+
json.JSONDecodeError: If config file is invalid JSON
|
35
|
+
ValueError: If config format is invalid
|
36
|
+
"""
|
37
|
+
if not os.path.exists(config_path):
|
38
|
+
raise FileNotFoundError(f"Filter configuration file not found: {config_path}")
|
39
|
+
|
40
|
+
try:
|
41
|
+
with open(config_path, encoding="utf-8") as f:
|
42
|
+
config = json.load(f)
|
43
|
+
except json.JSONDecodeError as e:
|
44
|
+
raise json.JSONDecodeError(f"Invalid JSON in filter config file {config_path}: {e}") from e
|
45
|
+
|
46
|
+
if not isinstance(config, dict):
|
47
|
+
raise ValueError(f"Filter configuration must be a JSON object/dictionary, got {type(config)}")
|
48
|
+
|
49
|
+
# Validate the configuration format
|
50
|
+
for path, methods in config.items():
|
51
|
+
if not isinstance(path, str):
|
52
|
+
raise ValueError(f"Path keys must be strings, got {type(path)} for key: {path}")
|
53
|
+
|
54
|
+
if isinstance(methods, str):
|
55
|
+
if methods != "all" and methods.lower() not in [
|
56
|
+
"get",
|
57
|
+
"post",
|
58
|
+
"put",
|
59
|
+
"delete",
|
60
|
+
"patch",
|
61
|
+
"head",
|
62
|
+
"options",
|
63
|
+
"trace",
|
64
|
+
]:
|
65
|
+
raise ValueError(f"Invalid method '{methods}' for path '{path}'. Use 'all' or valid HTTP methods.")
|
66
|
+
elif isinstance(methods, list):
|
67
|
+
for method in methods:
|
68
|
+
if not isinstance(method, str) or method.lower() not in [
|
69
|
+
"get",
|
70
|
+
"post",
|
71
|
+
"put",
|
72
|
+
"delete",
|
73
|
+
"patch",
|
74
|
+
"head",
|
75
|
+
"options",
|
76
|
+
"trace",
|
77
|
+
]:
|
78
|
+
raise ValueError(f"Invalid method '{method}' for path '{path}'. Use valid HTTP methods.")
|
79
|
+
else:
|
80
|
+
raise ValueError(f"Methods must be string or list of strings for path '{path}', got {type(methods)}")
|
81
|
+
|
82
|
+
logger.info(f"Loaded filter configuration with {len(config)} path specifications")
|
83
|
+
return config
|
84
|
+
|
85
|
+
|
86
|
+
def should_process_operation(path: str, method: str, filter_config: dict[str, str | list[str]] | None = None) -> bool:
|
87
|
+
"""
|
88
|
+
Check if a specific path+method combination should be processed based on filter config.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
path: The API path (e.g., "/users/{user-id}/profile")
|
92
|
+
method: The HTTP method (e.g., "get")
|
93
|
+
filter_config: Optional filter configuration dict
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
True if the operation should be processed, False otherwise
|
97
|
+
"""
|
98
|
+
if filter_config is None:
|
99
|
+
return True # No filter means process everything
|
100
|
+
|
101
|
+
if path not in filter_config:
|
102
|
+
return False # Path not in config means skip
|
103
|
+
|
104
|
+
allowed_methods = filter_config[path]
|
105
|
+
method_lower = method.lower()
|
106
|
+
|
107
|
+
if allowed_methods == "all":
|
108
|
+
return True
|
109
|
+
elif isinstance(allowed_methods, str):
|
110
|
+
return method_lower == allowed_methods.lower()
|
111
|
+
elif isinstance(allowed_methods, list):
|
112
|
+
return method_lower in [m.lower() for m in allowed_methods]
|
113
|
+
|
114
|
+
return False
|
@@ -9,6 +9,8 @@ import yaml
|
|
9
9
|
from jsonref import replace_refs
|
10
10
|
from pydantic import BaseModel
|
11
11
|
|
12
|
+
from .filters import load_filter_config, should_process_operation
|
13
|
+
|
12
14
|
|
13
15
|
class Parameters(BaseModel):
|
14
16
|
name: str
|
@@ -150,7 +152,13 @@ def _sanitize_identifier(name: str | None) -> str:
|
|
150
152
|
|
151
153
|
# Initial replacements for common non-alphanumeric characters
|
152
154
|
sanitized = (
|
153
|
-
name.replace("-", "_")
|
155
|
+
name.replace("-", "_")
|
156
|
+
.replace(".", "_")
|
157
|
+
.replace("[", "_")
|
158
|
+
.replace("]", "")
|
159
|
+
.replace("$", "_")
|
160
|
+
.replace("/", "_")
|
161
|
+
.replace("@", "at")
|
154
162
|
)
|
155
163
|
|
156
164
|
# Remove leading underscores, but preserve a single underscore if the name (after initial replace)
|
@@ -1012,18 +1020,28 @@ def load_schema(path: Path):
|
|
1012
1020
|
return _load_and_resolve_references(path)
|
1013
1021
|
|
1014
1022
|
|
1015
|
-
def generate_api_client(schema, class_name: str | None = None):
|
1023
|
+
def generate_api_client(schema, class_name: str | None = None, filter_config_path: str | None = None):
|
1016
1024
|
"""
|
1017
1025
|
Generate a Python API client class from an OpenAPI schema.
|
1018
1026
|
|
1019
1027
|
Args:
|
1020
1028
|
schema (dict): The OpenAPI schema as a dictionary.
|
1029
|
+
class_name (str | None): Optional class name override.
|
1030
|
+
filter_config_path (str | None): Optional path to JSON filter configuration file.
|
1021
1031
|
|
1022
1032
|
Returns:
|
1023
1033
|
str: A string containing the Python code for the API client class.
|
1024
1034
|
"""
|
1035
|
+
# Load filter configuration if provided
|
1036
|
+
filter_config = None
|
1037
|
+
if filter_config_path:
|
1038
|
+
filter_config = load_filter_config(filter_config_path)
|
1039
|
+
print(f"Loaded filter configuration from {filter_config_path} with {len(filter_config)} path specifications")
|
1040
|
+
|
1025
1041
|
methods = []
|
1026
1042
|
method_names = []
|
1043
|
+
processed_count = 0
|
1044
|
+
skipped_count = 0
|
1027
1045
|
|
1028
1046
|
# Extract API info for naming and base URL
|
1029
1047
|
info = schema.get("info", {})
|
@@ -1073,10 +1091,21 @@ def generate_api_client(schema, class_name: str | None = None):
|
|
1073
1091
|
for path, path_info in schema.get("paths", {}).items():
|
1074
1092
|
for method in path_info:
|
1075
1093
|
if method in ["get", "post", "put", "delete", "patch", "options", "head"]:
|
1094
|
+
# Apply filter configuration
|
1095
|
+
if not should_process_operation(path, method, filter_config):
|
1096
|
+
print(f"Skipping method generation for '{method.upper()} {path}' due to filter configuration.")
|
1097
|
+
skipped_count += 1
|
1098
|
+
continue
|
1099
|
+
|
1076
1100
|
operation = path_info[method]
|
1101
|
+
print(f"Generating method for: {method.upper()} {path}")
|
1077
1102
|
method_code, func_name = _generate_method_code(path, method, operation)
|
1078
1103
|
methods.append(method_code)
|
1079
1104
|
method_names.append(func_name)
|
1105
|
+
processed_count += 1
|
1106
|
+
|
1107
|
+
if filter_config is not None:
|
1108
|
+
print(f"Selective generation complete: {processed_count} methods generated, {skipped_count} methods skipped.")
|
1080
1109
|
|
1081
1110
|
# Generate list_tools method with all the function names
|
1082
1111
|
tools_list = ",\n ".join([f"self.{name}" for name in method_names])
|
@@ -12,6 +12,8 @@ import typer
|
|
12
12
|
import yaml
|
13
13
|
from rich.console import Console
|
14
14
|
|
15
|
+
from .filters import load_filter_config, should_process_operation
|
16
|
+
|
15
17
|
console = Console()
|
16
18
|
|
17
19
|
|
@@ -451,11 +453,12 @@ def simplify_parameter_context(parameter: dict) -> dict:
|
|
451
453
|
return simplified_context
|
452
454
|
|
453
455
|
|
454
|
-
def scan_schema_for_status(schema_data: dict):
|
456
|
+
def scan_schema_for_status(schema_data: dict, filter_config: dict[str, str | list[str]] | None = None):
|
455
457
|
"""
|
456
458
|
Scans the schema to report the status of descriptions/summaries
|
457
459
|
and identify critical issues like missing parameter 'name'/'in'.
|
458
460
|
Does NOT modify the schema or call the LLM.
|
461
|
+
Respects filter configuration if provided.
|
459
462
|
"""
|
460
463
|
logger.info("\n--- Scanning Schema for Status ---")
|
461
464
|
|
@@ -526,6 +529,12 @@ def scan_schema_for_status(schema_data: dict):
|
|
526
529
|
"trace",
|
527
530
|
]:
|
528
531
|
operation_location_base = f"paths.{path_key}.{method.lower()}"
|
532
|
+
|
533
|
+
# Apply filter configuration
|
534
|
+
if not should_process_operation(path_key, method, filter_config):
|
535
|
+
logger.debug(f"Skipping operation '{method.upper()} {path_key}' due to filter configuration.")
|
536
|
+
continue
|
537
|
+
|
529
538
|
if not isinstance(operation_value, dict):
|
530
539
|
logger.warning(f"Operation value for '{operation_location_base}' is not a dictionary. Skipping.")
|
531
540
|
continue
|
@@ -886,12 +895,20 @@ def process_operation(
|
|
886
895
|
|
887
896
|
|
888
897
|
def process_paths(
|
889
|
-
paths: dict,
|
898
|
+
paths: dict,
|
899
|
+
llm_model: str,
|
900
|
+
enhance_all: bool,
|
901
|
+
summaries_only: bool = False,
|
902
|
+
operation_ids_only: bool = False,
|
903
|
+
filter_config: dict[str, str | list[str]] | None = None,
|
890
904
|
):
|
891
905
|
if not isinstance(paths, dict):
|
892
906
|
logger.warning("'paths' field is not a dictionary. Skipping path processing.")
|
893
907
|
return
|
894
908
|
|
909
|
+
processed_count = 0
|
910
|
+
skipped_count = 0
|
911
|
+
|
895
912
|
for path_key, path_value in paths.items():
|
896
913
|
if path_key.lower().startswith("x-"):
|
897
914
|
logger.debug(f"Skipping processing of path extension '{path_key}'.")
|
@@ -909,9 +926,17 @@ def process_paths(
|
|
909
926
|
"patch",
|
910
927
|
"trace",
|
911
928
|
]:
|
929
|
+
# Apply filter configuration
|
930
|
+
if not should_process_operation(path_key, method, filter_config):
|
931
|
+
logger.debug(f"Skipping operation '{method.upper()} {path_key}' due to filter configuration.")
|
932
|
+
skipped_count += 1
|
933
|
+
continue
|
934
|
+
|
935
|
+
logger.info(f"Processing operation: {method.upper()} {path_key}")
|
912
936
|
process_operation(
|
913
937
|
operation_value, path_key, method, llm_model, enhance_all, summaries_only, operation_ids_only
|
914
938
|
)
|
939
|
+
processed_count += 1
|
915
940
|
elif method.lower().startswith("x-"):
|
916
941
|
logger.debug(f"Skipping processing of method extension '{method.lower()}' in path '{path_key}'.")
|
917
942
|
continue
|
@@ -928,6 +953,11 @@ def process_paths(
|
|
928
953
|
elif path_value is not None:
|
929
954
|
logger.warning(f"Path value for '{path_key}' is not a dictionary. Skipping processing.")
|
930
955
|
|
956
|
+
if filter_config is not None:
|
957
|
+
logger.info(
|
958
|
+
f"Selective processing complete: {processed_count} operations processed, {skipped_count} operations skipped."
|
959
|
+
)
|
960
|
+
|
931
961
|
|
932
962
|
def process_info_section(schema_data: dict, llm_model: str, enhance_all: bool): # New flag
|
933
963
|
info = schema_data.get("info")
|
@@ -1036,7 +1066,12 @@ def regenerate_duplicate_operation_ids(schema_data: dict, llm_model: str):
|
|
1036
1066
|
|
1037
1067
|
|
1038
1068
|
def preprocess_schema_with_llm(
|
1039
|
-
schema_data: dict,
|
1069
|
+
schema_data: dict,
|
1070
|
+
llm_model: str,
|
1071
|
+
enhance_all: bool,
|
1072
|
+
summaries_only: bool = False,
|
1073
|
+
operation_ids_only: bool = False,
|
1074
|
+
filter_config: dict[str, str | list[str]] | None = None,
|
1040
1075
|
):
|
1041
1076
|
"""
|
1042
1077
|
Processes the schema to add/enhance descriptions/summaries using an LLM.
|
@@ -1045,8 +1080,12 @@ def preprocess_schema_with_llm(
|
|
1045
1080
|
If operation_ids_only is True, only missing operationIds are generated (never overwritten).
|
1046
1081
|
Assumes basic schema structure validation (info, title) has already passed.
|
1047
1082
|
"""
|
1083
|
+
filter_info = ""
|
1084
|
+
if filter_config is not None:
|
1085
|
+
filter_info = f" | Selective processing: {len(filter_config)} path specifications"
|
1086
|
+
|
1048
1087
|
logger.info(
|
1049
|
-
f"\n--- Starting LLM Generation (enhance_all={enhance_all}, summaries_only={summaries_only}, operation_ids_only={operation_ids_only}) ---"
|
1088
|
+
f"\n--- Starting LLM Generation (enhance_all={enhance_all}, summaries_only={summaries_only}, operation_ids_only={operation_ids_only}){filter_info} ---"
|
1050
1089
|
)
|
1051
1090
|
|
1052
1091
|
# Only process info section if not operation_ids_only
|
@@ -1054,7 +1093,7 @@ def preprocess_schema_with_llm(
|
|
1054
1093
|
process_info_section(schema_data, llm_model, enhance_all)
|
1055
1094
|
|
1056
1095
|
paths = schema_data.get("paths")
|
1057
|
-
process_paths(paths, llm_model, enhance_all, summaries_only, operation_ids_only)
|
1096
|
+
process_paths(paths, llm_model, enhance_all, summaries_only, operation_ids_only, filter_config)
|
1058
1097
|
|
1059
1098
|
# After process_paths, regenerate_duplicate_operation_ids(schema_data, llm_model)
|
1060
1099
|
|
@@ -1066,10 +1105,24 @@ def run_preprocessing(
|
|
1066
1105
|
output_path: Path | None = None,
|
1067
1106
|
model: str = "perplexity/sonar",
|
1068
1107
|
debug: bool = False,
|
1108
|
+
filter_config_path: str | None = None,
|
1069
1109
|
):
|
1070
1110
|
set_logging_level("DEBUG" if debug else "INFO")
|
1071
1111
|
console.print("[bold blue]--- Starting OpenAPI Schema Preprocessor ---[/bold blue]")
|
1072
1112
|
|
1113
|
+
# Load filter configuration if provided
|
1114
|
+
filter_config = None
|
1115
|
+
if filter_config_path:
|
1116
|
+
try:
|
1117
|
+
filter_config = load_filter_config(filter_config_path)
|
1118
|
+
console.print("[bold cyan]Selective Processing Mode Enabled[/bold cyan]")
|
1119
|
+
console.print(f"[cyan]Filter configuration loaded from: {filter_config_path}[/cyan]")
|
1120
|
+
console.print(f"[cyan]Will process {len(filter_config)} path specifications[/cyan]")
|
1121
|
+
console.print()
|
1122
|
+
except (FileNotFoundError, json.JSONDecodeError, ValueError) as e:
|
1123
|
+
console.print(f"[red]Error loading filter configuration: {e}[/red]")
|
1124
|
+
raise typer.Exit(1) from e
|
1125
|
+
|
1073
1126
|
if schema_path is None:
|
1074
1127
|
path_str = typer.prompt(
|
1075
1128
|
"Please enter the path to the OpenAPI schema file (JSON or YAML)",
|
@@ -1090,7 +1143,7 @@ def run_preprocessing(
|
|
1090
1143
|
|
1091
1144
|
# --- Step 2: Scan and Report Status ---
|
1092
1145
|
try:
|
1093
|
-
scan_report = scan_schema_for_status(schema_data)
|
1146
|
+
scan_report = scan_schema_for_status(schema_data, filter_config)
|
1094
1147
|
report_scan_results(scan_report)
|
1095
1148
|
except Exception as e:
|
1096
1149
|
console.print(f"[red]An unexpected error occurred during schema scanning: {e}[/red]")
|
@@ -1224,7 +1277,9 @@ def run_preprocessing(
|
|
1224
1277
|
f"[blue]Starting LLM generation with Enhance All: {enhance_all}, Summaries Only: {summaries_only}, OperationIds Only: {operation_ids_only}[/blue]"
|
1225
1278
|
)
|
1226
1279
|
try:
|
1227
|
-
preprocess_schema_with_llm(
|
1280
|
+
preprocess_schema_with_llm(
|
1281
|
+
schema_data, model, enhance_all, summaries_only, operation_ids_only, filter_config
|
1282
|
+
)
|
1228
1283
|
console.print("[green]LLM generation complete.[/green]")
|
1229
1284
|
except Exception as e:
|
1230
1285
|
console.print(f"[red]Error during LLM generation: {e}[/red]")
|