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