lean-explore 0.3.0__py3-none-any.whl → 1.0.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.
Files changed (55) hide show
  1. lean_explore/__init__.py +14 -1
  2. lean_explore/api/__init__.py +12 -1
  3. lean_explore/api/client.py +64 -176
  4. lean_explore/cli/__init__.py +10 -1
  5. lean_explore/cli/data_commands.py +184 -489
  6. lean_explore/cli/display.py +171 -0
  7. lean_explore/cli/main.py +51 -608
  8. lean_explore/config.py +244 -0
  9. lean_explore/extract/__init__.py +5 -0
  10. lean_explore/extract/__main__.py +368 -0
  11. lean_explore/extract/doc_gen4.py +200 -0
  12. lean_explore/extract/doc_parser.py +499 -0
  13. lean_explore/extract/embeddings.py +369 -0
  14. lean_explore/extract/github.py +110 -0
  15. lean_explore/extract/index.py +316 -0
  16. lean_explore/extract/informalize.py +653 -0
  17. lean_explore/extract/package_config.py +59 -0
  18. lean_explore/extract/package_registry.py +45 -0
  19. lean_explore/extract/package_utils.py +105 -0
  20. lean_explore/extract/types.py +25 -0
  21. lean_explore/mcp/__init__.py +11 -1
  22. lean_explore/mcp/app.py +14 -46
  23. lean_explore/mcp/server.py +20 -35
  24. lean_explore/mcp/tools.py +71 -205
  25. lean_explore/models/__init__.py +9 -0
  26. lean_explore/models/search_db.py +76 -0
  27. lean_explore/models/search_types.py +53 -0
  28. lean_explore/search/__init__.py +32 -0
  29. lean_explore/search/engine.py +651 -0
  30. lean_explore/search/scoring.py +156 -0
  31. lean_explore/search/service.py +68 -0
  32. lean_explore/search/tokenization.py +71 -0
  33. lean_explore/util/__init__.py +28 -0
  34. lean_explore/util/embedding_client.py +92 -0
  35. lean_explore/util/logging.py +22 -0
  36. lean_explore/util/openrouter_client.py +63 -0
  37. lean_explore/util/reranker_client.py +187 -0
  38. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/METADATA +32 -9
  39. lean_explore-1.0.1.dist-info/RECORD +43 -0
  40. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/WHEEL +1 -1
  41. lean_explore-1.0.1.dist-info/entry_points.txt +2 -0
  42. lean_explore/cli/agent.py +0 -788
  43. lean_explore/cli/config_utils.py +0 -481
  44. lean_explore/defaults.py +0 -114
  45. lean_explore/local/__init__.py +0 -1
  46. lean_explore/local/search.py +0 -1050
  47. lean_explore/local/service.py +0 -479
  48. lean_explore/shared/__init__.py +0 -1
  49. lean_explore/shared/models/__init__.py +0 -1
  50. lean_explore/shared/models/api.py +0 -117
  51. lean_explore/shared/models/db.py +0 -396
  52. lean_explore-0.3.0.dist-info/RECORD +0 -26
  53. lean_explore-0.3.0.dist-info/entry_points.txt +0 -2
  54. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/licenses/LICENSE +0 -0
  55. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/top_level.txt +0 -0
lean_explore/cli/agent.py DELETED
@@ -1,788 +0,0 @@
1
- # src/lean_explore/cli/agent.py
2
-
3
- """Command-line interface logic for interacting with an AI agent.
4
-
5
- This module provides the agent_chat_command and supporting functions for the chat
6
- interaction, intended to be registered with a main Typer application.
7
- """
8
-
9
- import asyncio
10
- import functools
11
- import logging
12
- import os
13
- import pathlib
14
- import shutil
15
- import sys
16
- import textwrap
17
- from typing import Optional
18
-
19
- import typer
20
-
21
- # Ensure 'openai-agents' is installed
22
- try:
23
- from agents import Agent, Runner
24
- from agents.exceptions import AgentsException, UserError
25
- from agents.mcp import MCPServerStdio
26
- except ImportError:
27
- print(
28
- "Fatal Error: The 'openai-agents' library or its expected exceptions "
29
- "are not installed/found. Please install 'openai-agents' correctly "
30
- "(e.g., 'pip install openai-agents')",
31
- file=sys.stderr,
32
- )
33
- raise typer.Exit(code=1)
34
-
35
- try:
36
- from rich.console import Console
37
- from rich.panel import Panel
38
- from rich.text import Text
39
- except ImportError:
40
- print(
41
- "Fatal Error: The 'rich' library is not installed/found. "
42
- "Please install 'rich' (e.g., 'pip install rich')",
43
- file=sys.stderr,
44
- )
45
- raise typer.Exit(code=1)
46
-
47
-
48
- # Attempt to import your project's config_utils for API key loading
49
- config_utils_imported = False
50
- try:
51
- from lean_explore.cli import config_utils
52
-
53
- config_utils_imported = True
54
- except ImportError:
55
- # BasicConfig for this warning, actual command configures logger later
56
- logging.basicConfig(level=logging.WARNING)
57
- logging.warning(
58
- "Could not import 'lean_explore.cli.config_utils'. "
59
- "Automatic loading/saving of stored API keys will be disabled. "
60
- "Ensure 'lean_explore' package is installed correctly and accessible "
61
- "in PYTHONPATH (e.g., by running 'pip install -e .' from the project root)."
62
- )
63
-
64
- class _MockConfigUtils:
65
- """A mock for config_utils if it cannot be imported."""
66
-
67
- def load_api_key(self) -> Optional[str]:
68
- """Loads Lean Explore API key."""
69
- return None
70
-
71
- def load_openai_api_key(self) -> Optional[str]:
72
- """Loads OpenAI API key."""
73
- return None
74
-
75
- def save_api_key(self, api_key: str) -> bool:
76
- """Saves Lean Explore API key."""
77
- return False
78
-
79
- def save_openai_api_key(self, api_key: str) -> bool:
80
- """Saves OpenAI API key."""
81
- return False
82
-
83
- config_utils = _MockConfigUtils()
84
-
85
-
86
- # --- Async Wrapper for Typer Commands ---
87
- def typer_async(f):
88
- """A decorator to allow Typer commands to be async functions.
89
-
90
- It wraps the async function in `asyncio.run()`.
91
-
92
- Args:
93
- f: The asynchronous function to wrap.
94
-
95
- Returns:
96
- The wrapped function that can be called synchronously.
97
- """
98
-
99
- @functools.wraps(f)
100
- def wrapper(*args, **kwargs):
101
- return asyncio.run(f(*args, **kwargs))
102
-
103
- return wrapper
104
-
105
-
106
- # --- ANSI Color Codes ---
107
- class _Colors:
108
- """ANSI color codes for terminal output for enhanced readability."""
109
-
110
- BLUE = "\033[94m"
111
- GREEN = "\033[92m"
112
- YELLOW = "\033[93m"
113
- RED = "\033[91m"
114
- BOLD = "\033[1m"
115
- ENDC = "\033[0m"
116
-
117
-
118
- agent_cli_app = typer.Typer(
119
- name="agent_cli_utils",
120
- help="Utilities related to AI agent interactions.",
121
- no_args_is_help=True,
122
- )
123
-
124
- logger = logging.getLogger(__name__)
125
- console = Console()
126
- CHAT_CONTENT_WIDTH = 76 # Consistent with main.py's PANEL_CONTENT_WIDTH
127
-
128
-
129
- def _format_chat_text_for_panel(text_content: str, width: int) -> str:
130
- """Wraps text for chat display, padding lines to fill panel width.
131
-
132
- This function processes text line by line, wraps content that exceeds the
133
- specified width, and pads each resulting line with spaces to ensure a
134
- uniform block appearance within a Rich Panel. Empty lines in the input
135
- are preserved as padded blank lines.
136
-
137
- Args:
138
- text_content: The text content to wrap.
139
- width: The target width for text wrapping and padding.
140
-
141
- Returns:
142
- A string with wrapped and padded text.
143
- """
144
- if not text_content.strip():
145
- # For empty or whitespace-only input, provide a single padded blank line
146
- return " " * width
147
-
148
- input_lines = text_content.splitlines()
149
- output_panel_lines = []
150
-
151
- if not input_lines:
152
- return " " * width # Should be caught by strip(), but safeguard
153
-
154
- for line_text in input_lines:
155
- if not line_text.strip(): # An intentionally blank line in the input
156
- output_panel_lines.append(" " * width)
157
- else:
158
- wrapped_segments = textwrap.wrap(
159
- line_text,
160
- width=width,
161
- replace_whitespace=True, # Collapse multiple spaces within line
162
- drop_whitespace=True, # Remove leading/trailing space from segments
163
- break_long_words=True, # Break words that exceed width
164
- break_on_hyphens=True, # Allow breaking on hyphens
165
- )
166
- if not wrapped_segments:
167
- # If wrapping a non-blank line results in nothing (e.g. only whitespace)
168
- output_panel_lines.append(" " * width)
169
- else:
170
- for segment in wrapped_segments:
171
- output_panel_lines.append(segment.ljust(width))
172
-
173
- if not output_panel_lines:
174
- # Fallback if all processing led to no lines (e.g. input was "\n \n")
175
- return " " * width
176
-
177
- return "\n".join(output_panel_lines)
178
-
179
-
180
- def _handle_server_connection_error(
181
- error: Exception,
182
- lean_backend_type: str,
183
- debug_mode: bool,
184
- context: str = "server startup",
185
- ):
186
- """Handles MCP server connection errors by logging and user-friendly messages."""
187
- logger.error(
188
- f"CRITICAL: Error during MCP {context}: {type(error).__name__}: {error}",
189
- exc_info=debug_mode,
190
- )
191
-
192
- error_str = str(error).lower()
193
- is_timeout_error = "timed out" in error_str or "timeout" in error_str
194
-
195
- if is_timeout_error:
196
- console.print(
197
- Text.from_markup(
198
- "[bold red]Error: The Lean Explore server failed to start "
199
- "or respond promptly.[/bold red]"
200
- ),
201
- stderr=True,
202
- )
203
- if lean_backend_type == "local":
204
- console.print(
205
- Text.from_markup(
206
- "[yellow]This often occurs with the 'local' backend due to missing "
207
- "or corrupted data files.[/yellow]"
208
- ),
209
- stderr=True,
210
- )
211
- console.print(
212
- Text.from_markup("[yellow]Please try the following steps:[/yellow]"),
213
- stderr=True,
214
- )
215
- console.print(
216
- Text.from_markup(
217
- "[yellow] 1. Run 'leanexplore data fetch' to download or update "
218
- "the required data.[/yellow]"
219
- ),
220
- stderr=True,
221
- )
222
- console.print(
223
- Text.from_markup("[yellow] 2. Try this chat command again.[/yellow]"),
224
- stderr=True,
225
- )
226
- console.print(
227
- Text.from_markup(
228
- "[yellow] 3. If the problem persists, run 'leanexplore mcp serve "
229
- "--backend local --log-level DEBUG' directly in another terminal "
230
- "to see detailed server startup logs.[/yellow]"
231
- ),
232
- stderr=True,
233
- )
234
- else: # api backend or other cases
235
- console.print(
236
- Text.from_markup(
237
- "[yellow]Please check your network connection and ensure the API "
238
- "server is accessible.[/yellow]"
239
- ),
240
- stderr=True,
241
- )
242
- elif isinstance(error, UserError):
243
- console.print(
244
- Text.from_markup(
245
- f"[bold red]Error: SDK usage problem during {context}: "
246
- f"{error}[/bold red]"
247
- ),
248
- stderr=True,
249
- )
250
- elif isinstance(error, AgentsException):
251
- console.print(
252
- Text.from_markup(
253
- f"[bold red]Error: An SDK error occurred during {context}: "
254
- f"{error}[/bold red]"
255
- ),
256
- stderr=True,
257
- )
258
- else:
259
- console.print(
260
- Text.from_markup(
261
- f"[bold red]An unexpected error occurred during {context}: "
262
- f"{error}[/bold red]"
263
- ),
264
- stderr=True,
265
- )
266
-
267
- if debug_mode:
268
- console.print(
269
- Text.from_markup(
270
- f"[magenta]Error Details ({type(error).__name__}): {error}[/magenta]"
271
- ),
272
- stderr=True,
273
- )
274
- raise typer.Exit(code=1)
275
-
276
-
277
- # --- Core Agent Logic ---
278
- async def _run_agent_session(
279
- lean_backend_type: str,
280
- lean_explore_api_key_arg: Optional[str] = None,
281
- debug_mode: bool = False,
282
- log_level_for_mcp_server: str = "WARNING",
283
- ):
284
- """Internal function to set up and run the OpenAI Agent session.
285
-
286
- Args:
287
- lean_backend_type: The backend ('api' or 'local') for the Lean Explore server.
288
- lean_explore_api_key_arg: API key for Lean Explore (if 'api' backend),
289
- already resolved from CLI arg or ENV.
290
- debug_mode: If True, enables more verbose logging for this client and
291
- the MCP server.
292
- log_level_for_mcp_server: The log level to pass to the MCP server.
293
- """
294
- internal_server_script_path = (
295
- pathlib.Path(__file__).parent.parent / "mcp" / "server.py"
296
- ).resolve()
297
-
298
- # --- OpenAI API Key Acquisition ---
299
- openai_api_key = None
300
- if config_utils_imported:
301
- logger.debug("Attempting to load OpenAI API key from CLI configuration...")
302
- try:
303
- openai_api_key = config_utils.load_openai_api_key()
304
- if openai_api_key:
305
- logger.info("Loaded OpenAI API key from CLI configuration.")
306
- else:
307
- logger.debug("No OpenAI API key found in CLI configuration.")
308
- except Exception as e: # pylint: disable=broad-except
309
- logger.error(
310
- f"Error loading OpenAI API key from CLI configuration: {e}",
311
- exc_info=debug_mode,
312
- )
313
-
314
- if not openai_api_key:
315
- console.print(
316
- Text.from_markup(
317
- "[yellow]OpenAI API key not found in configuration.[/yellow]"
318
- )
319
- )
320
- openai_api_key = typer.prompt(
321
- "Please enter your OpenAI API key", hide_input=True
322
- )
323
- if not openai_api_key:
324
- console.print(
325
- Text.from_markup(
326
- "[bold red]OpenAI API key cannot be empty. Exiting.[/bold red]"
327
- ),
328
- stderr=True,
329
- )
330
- raise typer.Exit(code=1)
331
- logger.info("Using OpenAI API key provided via prompt.")
332
- if config_utils_imported:
333
- if typer.confirm(
334
- "Would you like to save this OpenAI API key for future use?"
335
- ):
336
- if config_utils.save_openai_api_key(openai_api_key):
337
- console.print(
338
- Text.from_markup(
339
- "[green]OpenAI API key saved successfully.[/green]"
340
- )
341
- )
342
- else:
343
- console.print(
344
- Text.from_markup("[red]Failed to save OpenAI API key.[/red]"),
345
- stderr=True,
346
- )
347
- else:
348
- console.print("OpenAI API key will be used for this session only.")
349
- else:
350
- console.print(
351
- Text.from_markup(
352
- "[yellow]Note: config_utils not available, "
353
- "OpenAI API key cannot be saved.[/yellow]"
354
- )
355
- )
356
-
357
- os.environ["OPENAI_API_KEY"] = openai_api_key
358
-
359
- # --- Lean Explore Server Script and Executable Validation ---
360
- if not internal_server_script_path.exists():
361
- error_msg = (
362
- "Lean Explore MCP server script not found at calculated path: "
363
- f"{internal_server_script_path}"
364
- )
365
- logger.error(error_msg)
366
- console.print(
367
- Text.from_markup(f"[bold red]Error: {error_msg}[/bold red]"), stderr=True
368
- )
369
- raise typer.Exit(code=1)
370
-
371
- python_executable = sys.executable
372
- if not python_executable or not shutil.which(python_executable):
373
- error_msg = (
374
- f"Python executable '{python_executable}' not found or not executable. "
375
- "Ensure Python is correctly installed and in your PATH."
376
- )
377
- logger.error(error_msg)
378
- console.print(
379
- Text.from_markup(f"[bold red]Error: {error_msg}[/bold red]"), stderr=True
380
- )
381
- raise typer.Exit(code=1)
382
-
383
- # --- Lean Explore API Key Acquisition (if API backend) ---
384
- effective_lean_api_key = lean_explore_api_key_arg
385
- if lean_backend_type == "api":
386
- if not effective_lean_api_key and config_utils_imported:
387
- logger.debug(
388
- "Lean Explore API key not provided via CLI option or ENV. "
389
- "Attempting to load from CLI configuration..."
390
- )
391
- try:
392
- stored_key = config_utils.load_api_key()
393
- if stored_key:
394
- effective_lean_api_key = stored_key
395
- logger.debug(
396
- "Successfully loaded Lean Explore API key from "
397
- "CLI configuration."
398
- )
399
- else:
400
- logger.debug("No Lean Explore API key found in CLI configuration.")
401
- except Exception as e: # pylint: disable=broad-except
402
- logger.error(
403
- f"Error loading Lean Explore API key from CLI configuration: {e}",
404
- exc_info=debug_mode,
405
- )
406
-
407
- if not effective_lean_api_key:
408
- console.print(
409
- Text.from_markup(
410
- "[yellow]Lean Explore API key is required for the 'api' backend "
411
- "and was not found through CLI option, environment variable, "
412
- "or configuration.[/yellow]"
413
- )
414
- )
415
- effective_lean_api_key = typer.prompt(
416
- "Please enter your Lean Explore API key", hide_input=True
417
- )
418
- if not effective_lean_api_key:
419
- console.print(
420
- Text.from_markup(
421
- "[bold red]Lean Explore API key cannot be empty for 'api' "
422
- "backend. Exiting.[/bold red]"
423
- ),
424
- stderr=True,
425
- )
426
- raise typer.Exit(code=1)
427
- logger.info("Using Lean Explore API key provided via prompt.")
428
- if config_utils_imported:
429
- if typer.confirm(
430
- "Would you like to save this Lean Explore API key for future use?"
431
- ):
432
- if config_utils.save_api_key(effective_lean_api_key):
433
- console.print(
434
- Text.from_markup(
435
- "[green]Lean Explore API key saved successfully."
436
- "[/green]"
437
- )
438
- )
439
- else:
440
- console.print(
441
- Text.from_markup(
442
- "[red]Failed to save Lean Explore API key.[/red]"
443
- ),
444
- stderr=True,
445
- )
446
- else:
447
- console.print(
448
- "Lean Explore API key will be used for this session only."
449
- )
450
- else:
451
- console.print(
452
- Text.from_markup(
453
- "[yellow]Note: config_utils not available, "
454
- "Lean Explore API key "
455
- "cannot be saved.[/yellow]"
456
- )
457
- )
458
-
459
- # --- MCP Server Setup ---
460
- mcp_server_args = [
461
- str(internal_server_script_path),
462
- "--backend",
463
- lean_backend_type,
464
- "--log-level",
465
- log_level_for_mcp_server,
466
- ]
467
- if lean_backend_type == "api" and effective_lean_api_key:
468
- mcp_server_args.extend(["--api-key", effective_lean_api_key])
469
-
470
- lean_explore_mcp_server = MCPServerStdio(
471
- name="LeanExploreSearchServer",
472
- params={
473
- "command": python_executable,
474
- "args": mcp_server_args,
475
- "cwd": str(internal_server_script_path.parent),
476
- },
477
- cache_tools_list=True,
478
- client_session_timeout_seconds=10.0,
479
- )
480
-
481
- # --- Agent Interaction Loop ---
482
- try:
483
- async with lean_explore_mcp_server as server_instance:
484
- logger.debug(
485
- f"MCP server '{server_instance.name}' connection initiated. "
486
- "Listing tools..."
487
- )
488
- tools = []
489
- try:
490
- tools = await server_instance.list_tools()
491
- if not tools or not any(tools):
492
- logger.warning(
493
- "MCP Server connected but reported no tools. "
494
- "Agent may lack expected capabilities."
495
- )
496
- else:
497
- logger.debug(
498
- f"Available tools from {server_instance.name}: "
499
- f"{[tool.name for tool in tools]}"
500
- )
501
- except (UserError, AgentsException, Exception) as e_list_tools:
502
- _handle_server_connection_error(
503
- e_list_tools, lean_backend_type, debug_mode, context="tool listing"
504
- )
505
-
506
- agent_model = "gpt-4.1"
507
- agent_object_name = "Assistant"
508
- agent_display_name = (
509
- f"{_Colors.BOLD}{_Colors.GREEN}{agent_object_name}{_Colors.ENDC}"
510
- )
511
-
512
- agent = Agent(
513
- name=agent_object_name,
514
- model=agent_model,
515
- instructions=(
516
- "You are a CLI assistant for searching a Lean 4 mathematical "
517
- "library.\n"
518
- "**Goal:** Find relevant Lean statements, understand them "
519
- "(including dependencies), and explain them conversationally "
520
- "to the user.\n"
521
- "**Output:** CLI-friendly (plain text, simple lists). "
522
- "NO complex Markdown/LaTeX.\n\n"
523
- "**Tool Usage & Efficiency:**\n"
524
- "* The `search`, `get_by_id`, and `get_dependencies` tools can "
525
- "all accept a list of inputs (queries or integer IDs) to "
526
- "perform batch operations. This is highly efficient. For "
527
- "example, `search(query=['query 1', 'query 2'])` or "
528
- "`get_by_id(group_id=[123, 456])`.\n"
529
- "* Always prefer making one batch call over multiple single "
530
- "calls.\n\n"
531
- "**Packages:** Use exact top-level names for filters (Batteries, "
532
- "Init, Lean, Mathlib, PhysLean, Std). Map subpackage mentions "
533
- "to top-level (e.g., 'Mathlib.Analysis' -> 'Mathlib').\n\n"
534
- "**Core Workflow:**\n"
535
- "1. **Search & Analyze:**\n"
536
- " * Execute multiple distinct `search` queries for each user "
537
- "request by passing a list of queries to the tool. Set `limit` "
538
- ">= 10 for each search.\n"
539
- " * From all search results, select the statement(s) most "
540
- "helpful to the user.\n"
541
- " * For each selected statement, use `get_dependencies` to "
542
- "understand its context. Do this efficiently by collecting all "
543
- "relevant IDs and passing them in a single list call.\n\n"
544
- "2. **Explain Results (Conversational & CLI-Friendly):**\n"
545
- " * Briefly state your search approach (e.g., 'I looked into X "
546
- "in Mathlib...').\n"
547
- " * For each selected statement:\n"
548
- " * Introduce it (e.g., Lean name: "
549
- "`primary_declaration.lean_name`).\n"
550
- " * Explain its meaning (use `docstring`, "
551
- "`informal_description`, `statement_text`).\n"
552
- " * Provide the full Lean code (`statement_text`).\n"
553
- " * Explain key dependencies (what they are, their role, "
554
- "using `statement_text` from `get_dependencies` output).\n"
555
- "3. **Specific User Follow-ups (If Asked):**\n"
556
- " * **`get_by_id`:** For one or more IDs, provide: ID, "
557
- "Lean name, statement text, source/line, docstring, informal "
558
- "description (structured CLI format).\n"
559
- " * **`get_dependencies` (Direct Request):** For one or more "
560
- "IDs, list dependencies for each: ID, Lean name, statement "
561
- "text/summary. State total count per ID.\n\n"
562
- "Always be concise, helpful, and clear."
563
- ),
564
- mcp_servers=[server_instance],
565
- )
566
- console.print(
567
- Text.from_markup(
568
- "[bold]Lean Search Assistant[/bold] (powered by [green]"
569
- f"{agent_model}[/green] and [green]{server_instance.name}[/green]) "
570
- "is ready."
571
- )
572
- )
573
- console.print(
574
- "Ask me to search for Lean statements (e.g., 'find definitions "
575
- "of a scheme')."
576
- )
577
- if not debug_mode and lean_backend_type == "local":
578
- console.print(
579
- Text.from_markup(
580
- "[yellow]Note: The local search server might print startup "
581
- "logs. "
582
- "For a quieter experience, use --debug to see detailed logs or "
583
- "ensure the server's default log level is WARNING.[/yellow]"
584
- )
585
- )
586
- console.print("Type 'exit' or 'quit' to end the session.")
587
- console.print()
588
-
589
- while True:
590
- try:
591
- user_styled_name = typer.style(
592
- "You", fg=typer.colors.BLUE, bold=True
593
- )
594
- user_input = typer.prompt(
595
- user_styled_name, default="", prompt_suffix=": "
596
- ).strip()
597
-
598
- if user_input.lower() in ["exit", "quit"]:
599
- logger.debug("Exiting chat loop.")
600
- break
601
- if not user_input:
602
- continue
603
-
604
- formatted_user_input = _format_chat_text_for_panel(
605
- user_input, CHAT_CONTENT_WIDTH
606
- )
607
- console.print(
608
- Panel(
609
- formatted_user_input,
610
- title="You",
611
- border_style="blue",
612
- title_align="left",
613
- expand=False,
614
- )
615
- )
616
- console.print()
617
-
618
- thinking_line_str_ansi = (
619
- f"{agent_display_name}: "
620
- f"{_Colors.YELLOW}Thinking...{_Colors.ENDC}"
621
- )
622
- sys.stdout.write(thinking_line_str_ansi)
623
- sys.stdout.flush()
624
-
625
- result = await Runner.run(starting_agent=agent, input=user_input)
626
-
627
- thinking_len_to_clear = Text.from_ansi(
628
- thinking_line_str_ansi
629
- ).cell_len
630
- sys.stdout.write("\r" + " " * thinking_len_to_clear + "\r")
631
- sys.stdout.flush()
632
-
633
- assistant_output = (
634
- "No specific textual output from the agent for this turn."
635
- )
636
- if result.final_output is not None:
637
- assistant_output = result.final_output
638
- else:
639
- logger.warning(
640
- "Agent run completed without error, but final_output "
641
- "is None."
642
- )
643
- assistant_output = (
644
- "(Agent action completed; no specific text message "
645
- "for this turn.)"
646
- )
647
-
648
- formatted_assistant_output = _format_chat_text_for_panel(
649
- assistant_output, CHAT_CONTENT_WIDTH
650
- )
651
- console.print(
652
- Panel(
653
- formatted_assistant_output,
654
- title=agent_object_name,
655
- border_style="green",
656
- title_align="left",
657
- expand=False,
658
- )
659
- )
660
- console.print()
661
-
662
- except typer.Abort:
663
- console.print(
664
- Text.from_markup(
665
- "\n[yellow]Chat interrupted by user. Exiting.[/yellow]"
666
- )
667
- )
668
- logger.debug("Chat interrupted by user (typer.Abort). Exiting.")
669
- break
670
- except KeyboardInterrupt:
671
- console.print(
672
- Text.from_markup(
673
- "\n[yellow]Chat interrupted by user. Exiting.[/yellow]"
674
- )
675
- )
676
- logger.debug(
677
- "Chat interrupted by user (KeyboardInterrupt). Exiting."
678
- )
679
- break
680
- except Exception as e: # pylint: disable=broad-except
681
- logger.error(
682
- f"An error occurred in the chat loop: {e}", exc_info=debug_mode
683
- )
684
- console.print(
685
- Text.from_markup(
686
- f"[bold red]An unexpected error occurred: {e}[/bold red]"
687
- )
688
- )
689
- break
690
- except (UserError, AgentsException, Exception) as e_startup:
691
- _handle_server_connection_error(
692
- e_startup,
693
- lean_backend_type,
694
- debug_mode,
695
- context="server startup or connection",
696
- )
697
-
698
- console.print(
699
- Text.from_markup("[bold]Lean Search Assistant session has ended.[/bold]")
700
- )
701
-
702
-
703
- @typer_async
704
- async def agent_chat_command(
705
- ctx: typer.Context,
706
- lean_backend: str = typer.Option(
707
- "api",
708
- "--backend",
709
- "-lb",
710
- help="Backend for the Lean Explore MCP server ('api' or 'local'). "
711
- "Default: api.",
712
- case_sensitive=False,
713
- ),
714
- lean_api_key: Optional[str] = typer.Option(
715
- None,
716
- "--lean-api-key",
717
- help="API key for Lean Explore (if 'api' backend). Overrides env var/config.",
718
- show_default=False,
719
- ),
720
- debug: bool = typer.Option(
721
- False,
722
- "--debug",
723
- help="Enable detailed debug logging for this script and the MCP server.",
724
- ),
725
- ):
726
- """Start an interactive chat session with the Lean Search Assistant.
727
-
728
- The assistant uses the Lean Explore MCP server to search for Lean statements.
729
- An OpenAI API key must be available (prompts if not found). If using `--backend api`
730
- (default), a Lean Explore API key is also needed (prompts if not found).
731
- """
732
- client_log_level = logging.DEBUG if debug else logging.INFO
733
- logging.basicConfig(
734
- level=client_log_level,
735
- format="%(asctime)s - %(levelname)s [%(name)s:%(lineno)s] %(message)s",
736
- datefmt="%Y-%m-%d %H:%M:%S",
737
- force=True,
738
- )
739
- logger.setLevel(client_log_level)
740
-
741
- library_log_level_for_client = logging.DEBUG if debug else logging.WARNING
742
- logging.getLogger("httpx").setLevel(library_log_level_for_client)
743
- logging.getLogger("httpcore").setLevel(library_log_level_for_client)
744
- logging.getLogger("openai").setLevel(library_log_level_for_client)
745
- logging.getLogger("agents").setLevel(library_log_level_for_client)
746
-
747
- mcp_server_log_level_str = "DEBUG" if debug else "WARNING"
748
-
749
- if not config_utils_imported and not debug:
750
- if not os.getenv("OPENAI_API_KEY"):
751
- console.print(
752
- Text.from_markup(
753
- "[yellow]Warning: Automatic loading of stored OpenAI API key "
754
- "is disabled (config module not found). OPENAI_API_KEY env "
755
- "var is not set. You will be prompted if no key is found "
756
- "in config.[/yellow]"
757
- ),
758
- stderr=True,
759
- )
760
- if lean_backend == "api" and not (
761
- lean_api_key or os.getenv("LEAN_EXPLORE_API_KEY")
762
- ):
763
- console.print(
764
- Text.from_markup(
765
- "[yellow]Warning: Automatic loading of stored Lean Explore "
766
- "API key is disabled (config module not found). If using "
767
- "--backend api, and key is not in env or via option, you "
768
- "will be prompted.[/yellow]"
769
- ),
770
- stderr=True,
771
- )
772
-
773
- resolved_lean_api_key = lean_api_key
774
- if resolved_lean_api_key is None and lean_backend == "api":
775
- env_key = os.getenv("LEAN_EXPLORE_API_KEY")
776
- if env_key:
777
- logger.debug(
778
- "Using Lean Explore API key from LEAN_EXPLORE_API_KEY environment "
779
- "variable for agent session."
780
- )
781
- resolved_lean_api_key = env_key
782
-
783
- await _run_agent_session(
784
- lean_backend_type=lean_backend,
785
- lean_explore_api_key_arg=resolved_lean_api_key,
786
- debug_mode=debug,
787
- log_level_for_mcp_server=mcp_server_log_level_str,
788
- )