lean-explore 0.2.2__py3-none-any.whl → 1.0.0__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.
- lean_explore/__init__.py +14 -1
- lean_explore/api/__init__.py +12 -1
- lean_explore/api/client.py +60 -80
- lean_explore/cli/__init__.py +10 -1
- lean_explore/cli/data_commands.py +157 -479
- lean_explore/cli/display.py +171 -0
- lean_explore/cli/main.py +51 -608
- lean_explore/config.py +244 -0
- lean_explore/extract/__init__.py +5 -0
- lean_explore/extract/__main__.py +368 -0
- lean_explore/extract/doc_gen4.py +200 -0
- lean_explore/extract/doc_parser.py +499 -0
- lean_explore/extract/embeddings.py +371 -0
- lean_explore/extract/github.py +110 -0
- lean_explore/extract/index.py +317 -0
- lean_explore/extract/informalize.py +653 -0
- lean_explore/extract/package_config.py +59 -0
- lean_explore/extract/package_registry.py +45 -0
- lean_explore/extract/package_utils.py +105 -0
- lean_explore/extract/types.py +25 -0
- lean_explore/mcp/__init__.py +11 -1
- lean_explore/mcp/app.py +14 -46
- lean_explore/mcp/server.py +20 -35
- lean_explore/mcp/tools.py +70 -177
- lean_explore/models/__init__.py +9 -0
- lean_explore/models/search_db.py +76 -0
- lean_explore/models/search_types.py +53 -0
- lean_explore/search/__init__.py +32 -0
- lean_explore/search/engine.py +655 -0
- lean_explore/search/scoring.py +156 -0
- lean_explore/search/service.py +68 -0
- lean_explore/search/tokenization.py +71 -0
- lean_explore/util/__init__.py +28 -0
- lean_explore/util/embedding_client.py +92 -0
- lean_explore/util/logging.py +22 -0
- lean_explore/util/openrouter_client.py +63 -0
- lean_explore/util/reranker_client.py +189 -0
- {lean_explore-0.2.2.dist-info → lean_explore-1.0.0.dist-info}/METADATA +55 -10
- lean_explore-1.0.0.dist-info/RECORD +43 -0
- {lean_explore-0.2.2.dist-info → lean_explore-1.0.0.dist-info}/WHEEL +1 -1
- lean_explore-1.0.0.dist-info/entry_points.txt +2 -0
- lean_explore/cli/agent.py +0 -781
- lean_explore/cli/config_utils.py +0 -481
- lean_explore/defaults.py +0 -114
- lean_explore/local/__init__.py +0 -1
- lean_explore/local/search.py +0 -1050
- lean_explore/local/service.py +0 -392
- lean_explore/shared/__init__.py +0 -1
- lean_explore/shared/models/__init__.py +0 -1
- lean_explore/shared/models/api.py +0 -117
- lean_explore/shared/models/db.py +0 -396
- lean_explore-0.2.2.dist-info/RECORD +0 -26
- lean_explore-0.2.2.dist-info/entry_points.txt +0 -2
- {lean_explore-0.2.2.dist-info → lean_explore-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {lean_explore-0.2.2.dist-info → lean_explore-1.0.0.dist-info}/top_level.txt +0 -0
lean_explore/cli/main.py
CHANGED
|
@@ -1,599 +1,87 @@
|
|
|
1
|
-
# src/lean_explore/cli/main.py
|
|
2
|
-
|
|
3
1
|
"""Command-Line Interface for Lean Explore.
|
|
4
2
|
|
|
5
|
-
Provides commands to
|
|
6
|
-
|
|
3
|
+
Provides commands to search for Lean declarations via the remote API,
|
|
4
|
+
interact with AI agents, and manage local data.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
13
12
|
|
|
14
|
-
import httpx
|
|
15
13
|
import typer
|
|
16
14
|
from rich.console import Console
|
|
17
|
-
from rich.panel import Panel
|
|
18
|
-
from rich.table import Table
|
|
19
15
|
|
|
20
|
-
from lean_explore.api
|
|
21
|
-
from lean_explore.cli import
|
|
22
|
-
|
|
23
|
-
data_commands, # For data management subcommands
|
|
24
|
-
)
|
|
25
|
-
from lean_explore.shared.models.api import APISearchResponse
|
|
16
|
+
from lean_explore.api import ApiClient
|
|
17
|
+
from lean_explore.cli import data_commands
|
|
18
|
+
from lean_explore.cli.display import display_search_results
|
|
26
19
|
|
|
27
|
-
|
|
28
|
-
from .agent import agent_chat_command
|
|
29
|
-
from .agent import typer_async as agent_typer_async
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
30
21
|
|
|
31
|
-
# Initialize Typer app and Rich console
|
|
32
22
|
app = typer.Typer(
|
|
33
|
-
name="
|
|
23
|
+
name="lean-explore",
|
|
34
24
|
help="A CLI tool to explore and search Lean mathematical libraries.",
|
|
35
25
|
add_completion=False,
|
|
36
|
-
rich_markup_mode="markdown",
|
|
26
|
+
rich_markup_mode="markdown",
|
|
37
27
|
)
|
|
38
|
-
configure_app = typer.Typer(
|
|
39
|
-
name="configure", help="Configure leanexplore CLI settings."
|
|
40
|
-
)
|
|
41
|
-
app.add_typer(configure_app)
|
|
42
28
|
|
|
43
29
|
mcp_app = typer.Typer(
|
|
44
30
|
name="mcp", help="Manage and run the Model Context Protocol (MCP) server."
|
|
45
31
|
)
|
|
46
32
|
app.add_typer(mcp_app)
|
|
47
33
|
|
|
48
|
-
# Add the data_commands.app as a subcommand group named "data"
|
|
49
34
|
app.add_typer(
|
|
50
35
|
data_commands.app,
|
|
51
36
|
name="data",
|
|
52
37
|
help="Manage local data toolchains.",
|
|
53
38
|
)
|
|
54
39
|
|
|
55
|
-
# Register the agent_chat_command directly on the main app as "chat"
|
|
56
|
-
# The agent_chat_command is already decorated with @typer_async in agent.py
|
|
57
|
-
app.command("chat", help="Interact with an AI agent using Lean Explore tools.")(
|
|
58
|
-
agent_chat_command
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
console = Console()
|
|
63
|
-
error_console = Console(stderr=True)
|
|
64
|
-
|
|
65
|
-
# Content width for panels.
|
|
66
|
-
PANEL_CONTENT_WIDTH = 80
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@configure_app.command("api-key")
|
|
70
|
-
def configure_lean_explore_api_key(
|
|
71
|
-
api_key: Optional[str] = typer.Option(
|
|
72
|
-
None,
|
|
73
|
-
prompt="Please enter your Lean Explore API key",
|
|
74
|
-
help="Your personal API key for accessing Lean Explore services.",
|
|
75
|
-
hide_input=True,
|
|
76
|
-
confirmation_prompt=True,
|
|
77
|
-
),
|
|
78
|
-
):
|
|
79
|
-
"""Configure and save your Lean Explore API key.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
api_key: The API key string to save. Prompts if not provided.
|
|
83
|
-
"""
|
|
84
|
-
if not api_key:
|
|
85
|
-
error_console.print(
|
|
86
|
-
"[bold red]Lean Explore API key cannot be empty.[/bold red]"
|
|
87
|
-
)
|
|
88
|
-
raise typer.Abort()
|
|
89
|
-
|
|
90
|
-
if config_utils.save_api_key(api_key):
|
|
91
|
-
config_path = config_utils.get_config_file_path()
|
|
92
|
-
console.print(
|
|
93
|
-
f"[bold green]Lean Explore API key saved successfully to: "
|
|
94
|
-
f"{config_path}[/bold green]"
|
|
95
|
-
)
|
|
96
|
-
else:
|
|
97
|
-
error_console.print(
|
|
98
|
-
"[bold red]Failed to save Lean Explore API key. "
|
|
99
|
-
"Check logs or permissions.[/bold red]"
|
|
100
|
-
)
|
|
101
|
-
raise typer.Abort()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@configure_app.command("openai-key")
|
|
105
|
-
def configure_openai_api_key(
|
|
106
|
-
api_key: Optional[str] = typer.Option(
|
|
107
|
-
None,
|
|
108
|
-
prompt="Please enter your OpenAI API key",
|
|
109
|
-
help="Your personal API key for OpenAI services (e.g., GPT-4).",
|
|
110
|
-
hide_input=True,
|
|
111
|
-
confirmation_prompt=True,
|
|
112
|
-
),
|
|
113
|
-
):
|
|
114
|
-
"""Configure and save your OpenAI API key.
|
|
115
|
-
|
|
116
|
-
This key is used by agent functionalities that leverage OpenAI models.
|
|
117
|
-
|
|
118
|
-
Args:
|
|
119
|
-
api_key: The OpenAI API key string to save. Prompts if not provided.
|
|
120
|
-
"""
|
|
121
|
-
if not api_key:
|
|
122
|
-
error_console.print("[bold red]OpenAI API key cannot be empty.[/bold red]")
|
|
123
|
-
raise typer.Abort()
|
|
124
|
-
|
|
125
|
-
if config_utils.save_openai_api_key(api_key):
|
|
126
|
-
config_path = config_utils.get_config_file_path()
|
|
127
|
-
console.print(
|
|
128
|
-
f"[bold green]OpenAI API key saved successfully to: "
|
|
129
|
-
f"{config_path}[/bold green]"
|
|
130
|
-
)
|
|
131
|
-
else:
|
|
132
|
-
error_console.print(
|
|
133
|
-
"[bold red]Failed to save OpenAI API key. "
|
|
134
|
-
"Check logs or permissions.[/bold red]"
|
|
135
|
-
)
|
|
136
|
-
raise typer.Abort()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _get_api_client() -> Optional[APIClient]:
|
|
140
|
-
"""Loads Lean Explore API key and initializes the APIClient.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
Optional[APIClient]: APIClient instance if key is found, None otherwise.
|
|
144
|
-
"""
|
|
145
|
-
api_key = config_utils.load_api_key()
|
|
146
|
-
if not api_key:
|
|
147
|
-
config_path = config_utils.get_config_file_path()
|
|
148
|
-
error_console.print(
|
|
149
|
-
"[bold yellow]Lean Explore API key not configured. Please run:"
|
|
150
|
-
"[/bold yellow]\n"
|
|
151
|
-
f" `leanexplore configure api-key`\n"
|
|
152
|
-
f"Your API key will be stored in: {config_path}"
|
|
153
|
-
)
|
|
154
|
-
return None
|
|
155
|
-
return APIClient(api_key=api_key)
|
|
156
|
-
|
|
157
40
|
|
|
158
|
-
def
|
|
159
|
-
"""
|
|
41
|
+
def _get_console(use_stderr: bool = False) -> Console:
|
|
42
|
+
"""Create a Rich console instance for output.
|
|
160
43
|
|
|
161
44
|
Args:
|
|
162
|
-
|
|
163
|
-
width: The target width for text wrapping and padding.
|
|
45
|
+
use_stderr: If True, output to stderr instead of stdout.
|
|
164
46
|
|
|
165
47
|
Returns:
|
|
166
|
-
A
|
|
48
|
+
A configured Console instance.
|
|
167
49
|
"""
|
|
168
|
-
|
|
169
|
-
return " " * width
|
|
170
|
-
|
|
171
|
-
final_output_lines = []
|
|
172
|
-
paragraphs = text_content.split("\n\n")
|
|
173
|
-
|
|
174
|
-
for i, paragraph in enumerate(paragraphs):
|
|
175
|
-
if not paragraph.strip() and i < len(paragraphs) - 1:
|
|
176
|
-
final_output_lines.append(" " * width)
|
|
177
|
-
continue
|
|
178
|
-
|
|
179
|
-
lines_in_paragraph = paragraph.splitlines()
|
|
180
|
-
if not lines_in_paragraph and paragraph.strip() == "":
|
|
181
|
-
final_output_lines.append(" " * width)
|
|
182
|
-
continue
|
|
183
|
-
if not lines_in_paragraph and not paragraph:
|
|
184
|
-
final_output_lines.append(" " * width)
|
|
185
|
-
continue
|
|
186
|
-
|
|
187
|
-
for line in lines_in_paragraph:
|
|
188
|
-
if not line.strip():
|
|
189
|
-
final_output_lines.append(" " * width)
|
|
190
|
-
continue
|
|
191
|
-
|
|
192
|
-
wrapped_segments = textwrap.wrap(
|
|
193
|
-
line,
|
|
194
|
-
width=width,
|
|
195
|
-
replace_whitespace=True,
|
|
196
|
-
drop_whitespace=True,
|
|
197
|
-
break_long_words=True,
|
|
198
|
-
break_on_hyphens=True,
|
|
199
|
-
)
|
|
200
|
-
if not wrapped_segments:
|
|
201
|
-
final_output_lines.append(" " * width)
|
|
202
|
-
else:
|
|
203
|
-
for segment in wrapped_segments:
|
|
204
|
-
final_output_lines.append(segment.ljust(width))
|
|
205
|
-
|
|
206
|
-
if i < len(paragraphs) - 1 and (
|
|
207
|
-
paragraph.strip() or (not paragraph.strip() and not lines_in_paragraph)
|
|
208
|
-
):
|
|
209
|
-
# Add a blank padded line between paragraphs
|
|
210
|
-
final_output_lines.append(" " * width)
|
|
211
|
-
|
|
212
|
-
if not final_output_lines and text_content.strip():
|
|
213
|
-
# Fallback for content that becomes empty after processing but was not initially
|
|
214
|
-
return " " * width
|
|
215
|
-
|
|
216
|
-
return "\n".join(final_output_lines)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _display_search_results(response: APISearchResponse, display_limit: int = 5):
|
|
220
|
-
"""Displays search results using fixed-width Panels for each item.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
response: The APISearchResponse object from the backend.
|
|
224
|
-
display_limit: The maximum number of individual results to display in detail.
|
|
225
|
-
"""
|
|
226
|
-
console.print(
|
|
227
|
-
Panel(
|
|
228
|
-
f"[bold cyan]Search Query:[/bold cyan] {response.query}",
|
|
229
|
-
expand=False,
|
|
230
|
-
border_style="dim",
|
|
231
|
-
)
|
|
232
|
-
)
|
|
233
|
-
if response.packages_applied:
|
|
234
|
-
console.print(
|
|
235
|
-
f"[bold cyan]Package Filters:[/bold cyan] "
|
|
236
|
-
f"{', '.join(response.packages_applied)}"
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
num_results_to_show = min(len(response.results), display_limit)
|
|
240
|
-
console.print(
|
|
241
|
-
f"Showing {num_results_to_show} of {response.count} "
|
|
242
|
-
f"(out of {response.total_candidates_considered} candidates considered by "
|
|
243
|
-
f"server). Time: {response.processing_time_ms}ms"
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
if not response.results:
|
|
247
|
-
console.print("[yellow]No results found.[/yellow]")
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
console.print("") # Adds a blank line for spacing
|
|
251
|
-
|
|
252
|
-
for i, item in enumerate(response.results):
|
|
253
|
-
if i >= display_limit:
|
|
254
|
-
break
|
|
255
|
-
|
|
256
|
-
lean_name = (
|
|
257
|
-
item.primary_declaration.lean_name if item.primary_declaration else "N/A"
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
console.rule(f"[bold]Result {i + 1}[/bold]", style="dim")
|
|
261
|
-
console.print(f"[bold cyan]ID:[/bold cyan] [dim]{item.id}[/dim]")
|
|
262
|
-
console.print(f"[bold cyan]Name:[/bold cyan] {lean_name}")
|
|
263
|
-
console.print(
|
|
264
|
-
f"[bold cyan]File:[/bold cyan] [green]{item.source_file}[/green]:"
|
|
265
|
-
f"[dim]{item.range_start_line}[/dim]"
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
code_to_display = item.display_statement_text or item.statement_text
|
|
269
|
-
if code_to_display:
|
|
270
|
-
formatted_code = _format_text_for_fixed_panel(
|
|
271
|
-
code_to_display, PANEL_CONTENT_WIDTH
|
|
272
|
-
)
|
|
273
|
-
console.print(
|
|
274
|
-
Panel(
|
|
275
|
-
formatted_code,
|
|
276
|
-
title="[bold green]Code[/bold green]",
|
|
277
|
-
border_style="green",
|
|
278
|
-
expand=False,
|
|
279
|
-
padding=(0, 1),
|
|
280
|
-
)
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
if item.docstring:
|
|
284
|
-
formatted_doc = _format_text_for_fixed_panel(
|
|
285
|
-
item.docstring, PANEL_CONTENT_WIDTH
|
|
286
|
-
)
|
|
287
|
-
console.print(
|
|
288
|
-
Panel(
|
|
289
|
-
formatted_doc,
|
|
290
|
-
title="[bold blue]Docstring[/bold blue]",
|
|
291
|
-
border_style="blue",
|
|
292
|
-
expand=False,
|
|
293
|
-
padding=(0, 1),
|
|
294
|
-
)
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
if item.informal_description:
|
|
298
|
-
formatted_informal = _format_text_for_fixed_panel(
|
|
299
|
-
item.informal_description, PANEL_CONTENT_WIDTH
|
|
300
|
-
)
|
|
301
|
-
console.print(
|
|
302
|
-
Panel(
|
|
303
|
-
formatted_informal,
|
|
304
|
-
title="[bold magenta]Informal Description[/bold magenta]",
|
|
305
|
-
border_style="magenta",
|
|
306
|
-
expand=False,
|
|
307
|
-
padding=(0, 1),
|
|
308
|
-
)
|
|
309
|
-
)
|
|
310
|
-
elif not item.docstring and not code_to_display:
|
|
311
|
-
console.print(
|
|
312
|
-
"[dim]No further textual details (docstring, informal description, "
|
|
313
|
-
"code) available for this item.[/dim]"
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
if i < num_results_to_show - 1: # Add spacing between items
|
|
317
|
-
console.print("")
|
|
318
|
-
|
|
319
|
-
console.rule(style="dim")
|
|
320
|
-
if len(response.results) > num_results_to_show:
|
|
321
|
-
console.print(
|
|
322
|
-
f"...and {len(response.results) - num_results_to_show} more results "
|
|
323
|
-
"received from server but not shown due to limit."
|
|
324
|
-
)
|
|
325
|
-
elif response.count > len(
|
|
326
|
-
response.results
|
|
327
|
-
): # Should be total_candidates_considered
|
|
328
|
-
console.print(
|
|
329
|
-
f"...and {response.total_candidates_considered - len(response.results)} "
|
|
330
|
-
"more results available "
|
|
331
|
-
"on server."
|
|
332
|
-
)
|
|
50
|
+
return Console(stderr=use_stderr)
|
|
333
51
|
|
|
334
52
|
|
|
335
53
|
@app.command("search")
|
|
336
|
-
|
|
337
|
-
async def search_command(
|
|
54
|
+
def search_command(
|
|
338
55
|
query_string: str = typer.Argument(..., help="The search query string."),
|
|
339
|
-
package: Optional[List[str]] = typer.Option(
|
|
340
|
-
None,
|
|
341
|
-
"--package",
|
|
342
|
-
"-p",
|
|
343
|
-
help="Filter by package name(s). Can be used multiple times.",
|
|
344
|
-
),
|
|
345
56
|
limit: int = typer.Option(
|
|
346
57
|
5, "--limit", "-n", help="Number of search results to display."
|
|
347
58
|
),
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
query_string: The natural language query to search for.
|
|
353
|
-
package: An optional list of package names to filter results by.
|
|
354
|
-
limit: The maximum number of search results to display to the user.
|
|
355
|
-
"""
|
|
356
|
-
client = _get_api_client()
|
|
357
|
-
if not client:
|
|
358
|
-
raise typer.Exit(code=1)
|
|
359
|
-
|
|
360
|
-
console.print(f"Searching for: '{query_string}'...")
|
|
361
|
-
try:
|
|
362
|
-
response = await client.search(query=query_string, package_filters=package)
|
|
363
|
-
_display_search_results(response, display_limit=limit)
|
|
364
|
-
except httpx.HTTPStatusError as e:
|
|
365
|
-
if e.response.status_code == 401:
|
|
366
|
-
error_console.print(
|
|
367
|
-
f"[bold red]API Error {e.response.status_code}: Unauthorized. "
|
|
368
|
-
"Your API key might be invalid or expired.[/bold red]"
|
|
369
|
-
)
|
|
370
|
-
error_console.print(
|
|
371
|
-
"Please reconfigure your API key using: `leanexplore configure api-key`"
|
|
372
|
-
)
|
|
373
|
-
else:
|
|
374
|
-
try:
|
|
375
|
-
error_detail = e.response.json().get("detail", e.response.text)
|
|
376
|
-
error_console.print(
|
|
377
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
378
|
-
f"{error_detail}[/bold red]"
|
|
379
|
-
)
|
|
380
|
-
except Exception:
|
|
381
|
-
error_console.print(
|
|
382
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
383
|
-
f"{e.response.text}[/bold red]"
|
|
384
|
-
)
|
|
385
|
-
raise typer.Exit(code=1)
|
|
386
|
-
except httpx.RequestError as e:
|
|
387
|
-
error_console.print(
|
|
388
|
-
f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
|
|
389
|
-
)
|
|
390
|
-
raise typer.Exit(code=1)
|
|
391
|
-
except Exception as e:
|
|
392
|
-
error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
|
|
393
|
-
raise typer.Exit(code=1)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
@app.command("get")
|
|
397
|
-
@agent_typer_async
|
|
398
|
-
async def get_by_id_command(
|
|
399
|
-
group_id: int = typer.Argument(
|
|
400
|
-
..., help="The ID of the statement group to retrieve."
|
|
59
|
+
packages: list[str] | None = typer.Option(
|
|
60
|
+
None, "--package", "-p", help="Filter by package (e.g., -p Mathlib -p Std)."
|
|
401
61
|
),
|
|
402
62
|
):
|
|
403
|
-
"""
|
|
404
|
-
|
|
405
|
-
Args:
|
|
406
|
-
group_id: The unique integer identifier of the statement group.
|
|
407
|
-
"""
|
|
408
|
-
client = _get_api_client()
|
|
409
|
-
if not client:
|
|
410
|
-
raise typer.Exit(code=1)
|
|
63
|
+
"""Search for Lean declarations using the Lean Explore API."""
|
|
64
|
+
asyncio.run(_search_async(query_string, limit, packages))
|
|
411
65
|
|
|
412
|
-
console.print(f"Fetching statement group ID: {group_id}...")
|
|
413
|
-
try:
|
|
414
|
-
item = await client.get_by_id(group_id)
|
|
415
|
-
if item:
|
|
416
|
-
console.print(
|
|
417
|
-
Panel(
|
|
418
|
-
f"[bold green]Statement Group ID: {item.id}[/bold green]",
|
|
419
|
-
expand=False,
|
|
420
|
-
border_style="dim",
|
|
421
|
-
)
|
|
422
|
-
)
|
|
423
|
-
lean_name = (
|
|
424
|
-
item.primary_declaration.lean_name
|
|
425
|
-
if item.primary_declaration
|
|
426
|
-
else "N/A"
|
|
427
|
-
)
|
|
428
|
-
console.print(f" [bold cyan]Lean Name:[/bold cyan] {lean_name}")
|
|
429
|
-
console.print(
|
|
430
|
-
f" [bold cyan]Source File:[/bold cyan] "
|
|
431
|
-
f"[green]{item.source_file}[/green]:"
|
|
432
|
-
f"[dim]{item.range_start_line}[/dim]"
|
|
433
|
-
)
|
|
434
66
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
formatted_stmt_text,
|
|
442
|
-
title="[bold green]Code[/bold green]",
|
|
443
|
-
border_style="green",
|
|
444
|
-
expand=False,
|
|
445
|
-
padding=(0, 1),
|
|
446
|
-
)
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
if item.docstring:
|
|
450
|
-
formatted_docstring = _format_text_for_fixed_panel(
|
|
451
|
-
item.docstring, PANEL_CONTENT_WIDTH
|
|
452
|
-
)
|
|
453
|
-
console.print(
|
|
454
|
-
Panel(
|
|
455
|
-
formatted_docstring,
|
|
456
|
-
title="[bold blue]Docstring[/bold blue]",
|
|
457
|
-
border_style="blue",
|
|
458
|
-
expand=False,
|
|
459
|
-
padding=(0, 1),
|
|
460
|
-
)
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
if item.informal_description:
|
|
464
|
-
formatted_informal = _format_text_for_fixed_panel(
|
|
465
|
-
item.informal_description, PANEL_CONTENT_WIDTH
|
|
466
|
-
)
|
|
467
|
-
console.print(
|
|
468
|
-
Panel(
|
|
469
|
-
formatted_informal,
|
|
470
|
-
title="[bold magenta]Informal Description[/bold magenta]",
|
|
471
|
-
border_style="magenta",
|
|
472
|
-
expand=False,
|
|
473
|
-
padding=(0, 1),
|
|
474
|
-
)
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
else:
|
|
478
|
-
error_console.print( # Changed to error_console for error/warning message
|
|
479
|
-
f"[yellow]Statement group with ID {group_id} not found.[/yellow]"
|
|
480
|
-
)
|
|
67
|
+
async def _search_async(
|
|
68
|
+
query_string: str, limit: int, packages: list[str] | None
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Async implementation of search command."""
|
|
71
|
+
console = _get_console()
|
|
72
|
+
error_console = _get_console(use_stderr=True)
|
|
481
73
|
|
|
482
|
-
except httpx.HTTPStatusError as e:
|
|
483
|
-
if e.response.status_code == 401:
|
|
484
|
-
error_console.print(
|
|
485
|
-
f"[bold red]API Error {e.response.status_code}: Unauthorized."
|
|
486
|
-
" Your API key might be invalid or expired.[/bold red]"
|
|
487
|
-
)
|
|
488
|
-
else:
|
|
489
|
-
try:
|
|
490
|
-
error_detail = e.response.json().get("detail", e.response.text)
|
|
491
|
-
error_console.print(
|
|
492
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
493
|
-
f"{error_detail}[/bold red]"
|
|
494
|
-
)
|
|
495
|
-
except Exception:
|
|
496
|
-
error_console.print(
|
|
497
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
498
|
-
f"{e.response.text}[/bold red]"
|
|
499
|
-
)
|
|
500
|
-
raise typer.Exit(code=1)
|
|
501
|
-
except httpx.RequestError as e:
|
|
502
|
-
error_console.print(
|
|
503
|
-
f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
|
|
504
|
-
)
|
|
505
|
-
raise typer.Exit(code=1)
|
|
506
|
-
except Exception as e:
|
|
507
|
-
error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
|
|
508
|
-
raise typer.Exit(code=1)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
@app.command("dependencies")
|
|
512
|
-
@agent_typer_async
|
|
513
|
-
async def get_dependencies_command(
|
|
514
|
-
group_id: int = typer.Argument(
|
|
515
|
-
..., help="The ID of the statement group to get dependencies for."
|
|
516
|
-
),
|
|
517
|
-
):
|
|
518
|
-
"""Get dependencies (citations) for a specific statement group by its ID.
|
|
519
|
-
|
|
520
|
-
Args:
|
|
521
|
-
group_id: The unique integer identifier of the statement group.
|
|
522
|
-
"""
|
|
523
|
-
client = _get_api_client()
|
|
524
|
-
if not client:
|
|
525
|
-
raise typer.Exit(code=1)
|
|
526
|
-
|
|
527
|
-
console.print(f"Fetching dependencies for statement group ID: {group_id}...")
|
|
528
74
|
try:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
f"[bold green]Citations for Statement Group ID: "
|
|
534
|
-
f"{response.source_group_id}[/bold green]",
|
|
535
|
-
expand=False,
|
|
536
|
-
border_style="dim",
|
|
537
|
-
)
|
|
538
|
-
)
|
|
539
|
-
console.print(f"Found {response.count} direct citations.")
|
|
540
|
-
|
|
541
|
-
if response.citations:
|
|
542
|
-
citation_table = Table(show_header=True, header_style="bold magenta")
|
|
543
|
-
citation_table.add_column("ID", style="dim", width=6)
|
|
544
|
-
citation_table.add_column("Cited Lean Name", width=40)
|
|
545
|
-
citation_table.add_column("File", style="green")
|
|
546
|
-
citation_table.add_column("Line", style="dim")
|
|
547
|
-
|
|
548
|
-
for item in response.citations:
|
|
549
|
-
lean_name = (
|
|
550
|
-
item.primary_declaration.lean_name
|
|
551
|
-
if item.primary_declaration
|
|
552
|
-
else "N/A"
|
|
553
|
-
)
|
|
554
|
-
citation_table.add_row(
|
|
555
|
-
str(item.id),
|
|
556
|
-
lean_name,
|
|
557
|
-
item.source_file,
|
|
558
|
-
str(item.range_start_line),
|
|
559
|
-
)
|
|
560
|
-
console.print(citation_table)
|
|
561
|
-
else:
|
|
562
|
-
console.print("[yellow]No citations found for this group.[/yellow]")
|
|
563
|
-
else:
|
|
564
|
-
error_console.print( # Changed to error_console for error/warning message
|
|
565
|
-
f"[yellow]Statement group with ID {group_id} not found or no "
|
|
566
|
-
"citations data available.[/yellow]"
|
|
567
|
-
)
|
|
568
|
-
|
|
569
|
-
except httpx.HTTPStatusError as e:
|
|
570
|
-
if e.response.status_code == 401:
|
|
571
|
-
error_console.print(
|
|
572
|
-
f"[bold red]API Error {e.response.status_code}: Unauthorized."
|
|
573
|
-
" Your API key might be invalid or expired.[/bold red]"
|
|
574
|
-
)
|
|
575
|
-
else:
|
|
576
|
-
try:
|
|
577
|
-
error_detail = e.response.json().get("detail", e.response.text)
|
|
578
|
-
error_console.print(
|
|
579
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
580
|
-
f"{error_detail}[/bold red]"
|
|
581
|
-
)
|
|
582
|
-
except Exception:
|
|
583
|
-
error_console.print(
|
|
584
|
-
f"[bold red]API Error {e.response.status_code}: "
|
|
585
|
-
f"{e.response.text}[/bold red]"
|
|
586
|
-
)
|
|
587
|
-
raise typer.Exit(code=1)
|
|
588
|
-
except httpx.RequestError as e:
|
|
589
|
-
error_console.print(
|
|
590
|
-
f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
|
|
591
|
-
)
|
|
592
|
-
raise typer.Exit(code=1)
|
|
593
|
-
except Exception as e:
|
|
594
|
-
error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
|
|
75
|
+
client = ApiClient()
|
|
76
|
+
except ValueError as error:
|
|
77
|
+
logger.error("Failed to initialize API client: %s", error)
|
|
78
|
+
error_console.print(f"[bold red]Error: {error}[/bold red]")
|
|
595
79
|
raise typer.Exit(code=1)
|
|
596
80
|
|
|
81
|
+
console.print(f"Searching for: '{query_string}'...")
|
|
82
|
+
response = await client.search(query=query_string, limit=limit, packages=packages)
|
|
83
|
+
display_search_results(response, display_limit=limit, console=console)
|
|
84
|
+
|
|
597
85
|
|
|
598
86
|
@mcp_app.command("serve")
|
|
599
87
|
def mcp_serve_command(
|
|
@@ -605,23 +93,15 @@ def mcp_serve_command(
|
|
|
605
93
|
case_sensitive=False,
|
|
606
94
|
show_choices=True,
|
|
607
95
|
),
|
|
608
|
-
api_key_override:
|
|
96
|
+
api_key_override: str | None = typer.Option(
|
|
609
97
|
None,
|
|
610
98
|
"--api-key",
|
|
611
|
-
help="API key to use if backend is 'api'. Overrides
|
|
612
|
-
"Not used for 'local' backend.",
|
|
99
|
+
help="API key to use if backend is 'api'. Overrides env var.",
|
|
613
100
|
),
|
|
614
101
|
):
|
|
615
|
-
"""Launch the Lean Explore MCP (Model Context Protocol) server.
|
|
616
|
-
|
|
617
|
-
The server communicates via stdio and provides Lean search functionalities
|
|
618
|
-
as MCP tools. The actual checks for local data presence or API key validity
|
|
619
|
-
are handled by the 'lean_explore.mcp.server' module when it starts.
|
|
102
|
+
"""Launch the Lean Explore MCP (Model Context Protocol) server."""
|
|
103
|
+
error_console = _get_console(use_stderr=True)
|
|
620
104
|
|
|
621
|
-
Args:
|
|
622
|
-
backend: The backend choice ('api' or 'local').
|
|
623
|
-
api_key_override: Optional API key to override any stored key.
|
|
624
|
-
"""
|
|
625
105
|
command_parts = [
|
|
626
106
|
sys.executable,
|
|
627
107
|
"-m",
|
|
@@ -631,60 +111,23 @@ def mcp_serve_command(
|
|
|
631
111
|
]
|
|
632
112
|
|
|
633
113
|
if backend.lower() == "api":
|
|
634
|
-
|
|
635
|
-
if not
|
|
114
|
+
effective_api_key = api_key_override or os.getenv("LEANEXPLORE_API_KEY")
|
|
115
|
+
if not effective_api_key:
|
|
116
|
+
logger.error("API key required for 'api' backend but not provided")
|
|
636
117
|
error_console.print(
|
|
637
|
-
"[bold red]
|
|
638
|
-
"
|
|
639
|
-
"Please configure it using `leanexplore configure api-key` "
|
|
640
|
-
"or provide it with the `--api-key` option for this command."
|
|
118
|
+
"[bold red]API key required for 'api' backend.[/bold red]\n"
|
|
119
|
+
"Set LEANEXPLORE_API_KEY or use --api-key option."
|
|
641
120
|
)
|
|
642
121
|
raise typer.Abort()
|
|
643
122
|
if api_key_override:
|
|
644
123
|
command_parts.extend(["--api-key", api_key_override])
|
|
645
|
-
elif backend.lower() == "local":
|
|
646
|
-
error_console.print( # Changed to error_console for consistency
|
|
647
|
-
"[dim]Attempting to start MCP server with 'local' backend. "
|
|
648
|
-
"The server will verify local data availability.[/dim]"
|
|
649
|
-
)
|
|
650
|
-
else:
|
|
651
|
-
error_console.print(
|
|
652
|
-
f"[bold red]Invalid backend: '{backend}'. Must be 'api' or 'local'."
|
|
653
|
-
"[/bold red]"
|
|
654
|
-
)
|
|
655
|
-
raise typer.Abort()
|
|
656
124
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
)
|
|
660
|
-
error_console.print(
|
|
661
|
-
"[dim]The server will now take over stdio. To stop it, the connected MCP "
|
|
662
|
-
"client should disconnect, or you may need to manually terminate this process "
|
|
663
|
-
"(e.g., Ctrl+C if no client is managing it).[/dim]"
|
|
664
|
-
)
|
|
125
|
+
logger.info("Starting MCP server with backend: %s", backend.lower())
|
|
126
|
+
result = subprocess.run(command_parts, check=False)
|
|
665
127
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
error_console.print(
|
|
670
|
-
f"[bold red]MCP server subprocess exited with code: "
|
|
671
|
-
f"{process_result.returncode}. Check server logs above for "
|
|
672
|
-
f"details.[/bold red]"
|
|
673
|
-
)
|
|
674
|
-
except FileNotFoundError:
|
|
675
|
-
error_console.print(
|
|
676
|
-
f"[bold red]Error: Could not find Python interpreter '{sys.executable}' "
|
|
677
|
-
f"or the MCP server module 'lean_explore.mcp.server'.[/bold red]"
|
|
678
|
-
)
|
|
679
|
-
error_console.print(
|
|
680
|
-
"Please ensure the package is installed correctly and "
|
|
681
|
-
"`python -m lean_explore.mcp.server` is runnable."
|
|
682
|
-
)
|
|
683
|
-
except Exception as e:
|
|
684
|
-
error_console.print(
|
|
685
|
-
f"[bold red]An error occurred while trying to launch or run the MCP "
|
|
686
|
-
f"server: {e}[/bold red]"
|
|
687
|
-
)
|
|
128
|
+
if result.returncode != 0:
|
|
129
|
+
logger.error("MCP server exited with code %d", result.returncode)
|
|
130
|
+
raise typer.Exit(code=result.returncode)
|
|
688
131
|
|
|
689
132
|
|
|
690
133
|
if __name__ == "__main__":
|