janito 1.10.0__tar.gz → 1.11.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {janito-1.10.0/janito.egg-info → janito-1.11.1}/PKG-INFO +1 -1
- janito-1.11.1/janito/__init__.py +1 -0
- janito-1.11.1/janito/agent/conversation_api.py +306 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_ui.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/llm_conversation_history.py +12 -0
- janito-1.11.1/janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +30 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/__init__.py +2 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/create_directory.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/create_file.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/fetch_url.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/find_files.py +26 -13
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/core.py +1 -1
- janito-1.11.1/janito/agent/tools/get_file_outline/python_outline.py +178 -0
- janito-1.11.1/janito/agent/tools/get_lines.py +149 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/move_file.py +58 -32
- janito-1.11.1/janito/agent/tools/open_url.py +31 -0
- janito-1.11.1/janito/agent/tools/python_command_runner.py +149 -0
- janito-1.11.1/janito/agent/tools/python_file_runner.py +147 -0
- janito-1.11.1/janito/agent/tools/python_stdin_runner.py +153 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/remove_directory.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/remove_file.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/replace_file.py +2 -2
- janito-1.11.1/janito/agent/tools/replace_text_in_file.py +262 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/run_bash_command.py +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/run_powershell_command.py +4 -0
- janito-1.11.1/janito/agent/tools/search_text/__init__.py +1 -0
- janito-1.11.1/janito/agent/tools/search_text/core.py +176 -0
- janito-1.11.1/janito/agent/tools/search_text/match_lines.py +58 -0
- janito-1.11.1/janito/agent/tools/search_text/pattern_utils.py +65 -0
- janito-1.11.1/janito/agent/tools/search_text/traverse_directory.py +132 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/core.py +41 -30
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
- janito-1.11.1/janito/agent/tools/validate_file_syntax/markdown_validator.py +109 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/gitignore_utils.py +25 -2
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/utils.py +7 -1
- janito-1.11.1/janito/cli/config_commands.py +211 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/main.py +51 -8
- janito-1.11.1/janito/shell/session/config.py +109 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/ui/interactive.py +97 -73
- janito-1.11.1/janito/termweb/static/editor.css +145 -0
- janito-1.10.0/janito/termweb/static/editor.css → janito-1.11.1/janito/termweb/static/editor.css.bak +30 -27
- janito-1.10.0/janito/termweb/static/editor.html.bak → janito-1.11.1/janito/termweb/static/editor.html +17 -11
- janito-1.10.0/janito/termweb/static/editor.html → janito-1.11.1/janito/termweb/static/editor.html.bak +11 -7
- janito-1.10.0/janito/termweb/static/editor.js.bak → janito-1.11.1/janito/termweb/static/editor.js +101 -65
- janito-1.10.0/janito/termweb/static/editor.js → janito-1.11.1/janito/termweb/static/editor.js.bak +90 -40
- janito-1.10.0/janito/termweb/static/index.html.bak → janito-1.11.1/janito/termweb/static/index.html +1 -2
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.css +1 -22
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.css.bak +6 -4
- janito-1.10.0/janito/termweb/static/termweb.js.bak → janito-1.11.1/janito/termweb/static/termweb.js +1 -8
- {janito-1.10.0 → janito-1.11.1/janito.egg-info}/PKG-INFO +1 -1
- {janito-1.10.0 → janito-1.11.1}/janito.egg-info/SOURCES.txt +6 -1
- {janito-1.10.0 → janito-1.11.1}/pyproject.toml +1 -1
- janito-1.10.0/janito/__init__.py +0 -1
- janito-1.10.0/janito/agent/conversation_api.py +0 -218
- janito-1.10.0/janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +0 -15
- janito-1.10.0/janito/agent/tools/get_file_outline/python_outline.py +0 -134
- janito-1.10.0/janito/agent/tools/get_lines.py +0 -120
- janito-1.10.0/janito/agent/tools/python_command_runner.py +0 -150
- janito-1.10.0/janito/agent/tools/python_file_runner.py +0 -148
- janito-1.10.0/janito/agent/tools/python_stdin_runner.py +0 -154
- janito-1.10.0/janito/agent/tools/replace_text_in_file.py +0 -218
- janito-1.10.0/janito/agent/tools/search_text.py +0 -254
- janito-1.10.0/janito/agent/tools/validate_file_syntax/markdown_validator.py +0 -66
- janito-1.10.0/janito/cli/config_commands.py +0 -208
- janito-1.10.0/janito/shell/session/config.py +0 -101
- janito-1.10.0/janito/termweb/static/editor.css.bak +0 -27
- {janito-1.10.0 → janito-1.11.1}/LICENSE +0 -0
- {janito-1.10.0 → janito-1.11.1}/MANIFEST.in +0 -0
- {janito-1.10.0 → janito-1.11.1}/README.md +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/__main__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/api_exceptions.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/config.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/config_defaults.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/config_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/content_handler.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_exceptions.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_tool_calls.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/event.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/event_dispatcher.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/event_handler_protocol.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/event_system.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/message_handler.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/message_handler_protocol.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/openai_client.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/openai_schema_generator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/platform_discovery.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/profile_manager.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/queued_message_handler.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/rich_live.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/rich_message_handler.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/runtime_config.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/test_handler_protocols.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/test_openai_schema_generator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tests/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_base.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_executor.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_registry.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_use_tracker.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/ask_user.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/delete_text_in_file.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/markdown_outline.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/search_outline.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/present_choices.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/css_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/js_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/json_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/ps1_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/python_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/xml_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/yaml_validator.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/action_type.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/dir_walk_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/formatting.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/test_gitignore_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/_livereload_log_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/_print_config.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/_termweb_log_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/arg_parser.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/cli_main.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/config_runner.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/formatting_runner.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/livereload_starter.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/logging_setup.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/main.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/one_shot.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/cli/termweb_starter.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/i18n/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/i18n/messages.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/i18n/pt.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/livereload/app.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/rich_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/config.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/conversation_restart.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/edit.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/history_view.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/lang.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/livelogs.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/prompt.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/session.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/session_control.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/termweb_log.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/tools.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/track.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/utility.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/verbose.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/commands.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/input_history.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/completer.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/load_prompt.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/session_setup.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/session/history.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/shell/session/manager.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/app.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/explorer.html.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/favicon.ico +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/favicon.ico.bak +0 -0
- /janito-1.10.0/janito/termweb/static/index.html → /janito-1.11.1/janito/termweb/static/index.html.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/index.html.bak.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/landing.html.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termicon.svg +0 -0
- /janito-1.10.0/janito/termweb/static/termweb.js → /janito-1.11.1/janito/termweb/static/termweb.js.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.js.bak.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb_quickopen.js +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb_quickopen.js.bak +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/tests/test_rich_utils.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/web/__init__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/web/__main__.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito/web/app.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito.egg-info/dependency_links.txt +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito.egg-info/entry_points.txt +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito.egg-info/requires.txt +0 -0
- {janito-1.10.0 → janito-1.11.1}/janito.egg-info/top_level.txt +0 -0
- {janito-1.10.0 → janito-1.11.1}/setup.cfg +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_basic.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_find_files.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_outline_formatter.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_outline_no_overlap.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_outline_python_file.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_outline_python_file_complex.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_outline_tool_run.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_platform_discovery.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_python_command_runner.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_python_file_runner.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_python_stdin_runner.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_rich_message_handler_action_type.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_run_powershell_command.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_search_text.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_set_role.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_docstring_formats.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_manual_sim.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_validation.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_tool_use_tracker.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_validate_file_syntax.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_validate_file_syntax_xml_html.py +0 -0
- {janito-1.10.0 → janito-1.11.1}/tests/test_validate_markdown_syntax.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "1.11.0"
|
@@ -0,0 +1,306 @@
|
|
1
|
+
"""
|
2
|
+
Handles OpenAI API calls and retry logic for conversation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
from janito.i18n import tr
|
7
|
+
import json
|
8
|
+
from janito.agent.runtime_config import runtime_config
|
9
|
+
from janito.agent.tool_registry import get_tool_schemas
|
10
|
+
from janito.agent.conversation_exceptions import NoToolSupportError, EmptyResponseError
|
11
|
+
from janito.agent.api_exceptions import ApiError
|
12
|
+
from rich.console import Console
|
13
|
+
from rich.status import Status
|
14
|
+
|
15
|
+
console = Console()
|
16
|
+
|
17
|
+
|
18
|
+
def _sanitize_utf8_surrogates(obj):
|
19
|
+
if isinstance(obj, dict):
|
20
|
+
return {k: _sanitize_utf8_surrogates(v) for k, v in obj.items()}
|
21
|
+
elif isinstance(obj, list):
|
22
|
+
return [_sanitize_utf8_surrogates(i) for i in obj]
|
23
|
+
elif isinstance(obj, str):
|
24
|
+
return obj.encode("utf-8", "surrogatepass").decode("utf-8", "ignore")
|
25
|
+
else:
|
26
|
+
return obj
|
27
|
+
|
28
|
+
|
29
|
+
def get_openai_response(
|
30
|
+
client, model, messages, max_tokens, tools=None, tool_choice=None, temperature=None
|
31
|
+
):
|
32
|
+
"""OpenAI API call."""
|
33
|
+
messages = _sanitize_utf8_surrogates(messages)
|
34
|
+
from janito.agent.conversation_exceptions import ProviderError
|
35
|
+
|
36
|
+
if runtime_config.get("vanilla_mode", False):
|
37
|
+
response = client.chat.completions.create(
|
38
|
+
model=model,
|
39
|
+
messages=messages,
|
40
|
+
max_tokens=max_tokens,
|
41
|
+
)
|
42
|
+
else:
|
43
|
+
response = client.chat.completions.create(
|
44
|
+
model=model,
|
45
|
+
messages=messages,
|
46
|
+
tools=tools or get_tool_schemas(),
|
47
|
+
tool_choice=tool_choice or "auto",
|
48
|
+
temperature=temperature if temperature is not None else 0.2,
|
49
|
+
max_tokens=max_tokens,
|
50
|
+
)
|
51
|
+
# Explicitly check for missing or empty choices (API/LLM error)
|
52
|
+
if (
|
53
|
+
not hasattr(response, "choices")
|
54
|
+
or response.choices is None
|
55
|
+
or len(response.choices) == 0
|
56
|
+
):
|
57
|
+
# Always check for error before raising ProviderError
|
58
|
+
error = getattr(response, "error", None)
|
59
|
+
if error:
|
60
|
+
print(f"ApiError: {error.get('message', error)}")
|
61
|
+
print(f"Full error object: {error}")
|
62
|
+
print(f"Raw response: {response}")
|
63
|
+
raise ApiError(error.get("message", str(error)))
|
64
|
+
raise ProviderError(
|
65
|
+
f"No choices in response; possible API or LLM error. Raw response: {response!r}",
|
66
|
+
{"code": 502, "raw_response": str(response)},
|
67
|
+
)
|
68
|
+
return response
|
69
|
+
|
70
|
+
|
71
|
+
def _extract_status_and_retry_after(e, error_message):
|
72
|
+
status_code = None
|
73
|
+
retry_after = None
|
74
|
+
if hasattr(e, "status_code"):
|
75
|
+
status_code = getattr(e, "status_code")
|
76
|
+
elif hasattr(e, "response") and hasattr(e.response, "status_code"):
|
77
|
+
status_code = getattr(e.response, "status_code")
|
78
|
+
elif "429" in error_message:
|
79
|
+
status_code = 429
|
80
|
+
import re
|
81
|
+
|
82
|
+
match = re.search(r"status[ _]?code[=: ]+([0-9]+)", error_message)
|
83
|
+
if match:
|
84
|
+
status_code = int(match.group(1))
|
85
|
+
match_retry = re.search(r"retry[-_ ]?after[=: ]+([0-9]+)", error_message)
|
86
|
+
if match_retry:
|
87
|
+
retry_after = int(match_retry.group(1))
|
88
|
+
return status_code, retry_after
|
89
|
+
|
90
|
+
|
91
|
+
def _calculate_wait_time(status_code, retry_after, attempt):
|
92
|
+
if status_code == 429 and retry_after:
|
93
|
+
return max(retry_after, 2**attempt)
|
94
|
+
return 2**attempt
|
95
|
+
|
96
|
+
|
97
|
+
def _log_and_sleep(
|
98
|
+
message,
|
99
|
+
attempt,
|
100
|
+
max_retries,
|
101
|
+
e=None,
|
102
|
+
wait_time=None,
|
103
|
+
status=None,
|
104
|
+
waiting_message=None,
|
105
|
+
restore_message=None,
|
106
|
+
):
|
107
|
+
status_message = tr(
|
108
|
+
message,
|
109
|
+
attempt=attempt,
|
110
|
+
max_retries=max_retries,
|
111
|
+
e=e,
|
112
|
+
wait_time=wait_time,
|
113
|
+
)
|
114
|
+
if (
|
115
|
+
status is not None
|
116
|
+
and waiting_message is not None
|
117
|
+
and restore_message is not None
|
118
|
+
):
|
119
|
+
original_message = status.status
|
120
|
+
status.update(waiting_message)
|
121
|
+
time.sleep(wait_time)
|
122
|
+
status.update(restore_message)
|
123
|
+
else:
|
124
|
+
with Status(status_message, console=console, spinner="dots"):
|
125
|
+
time.sleep(wait_time)
|
126
|
+
|
127
|
+
|
128
|
+
def _handle_json_decode_error(e, attempt, max_retries, status=None):
|
129
|
+
if attempt < max_retries:
|
130
|
+
wait_time = 2**attempt
|
131
|
+
if status is not None:
|
132
|
+
_log_and_sleep(
|
133
|
+
"Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
|
134
|
+
attempt,
|
135
|
+
max_retries,
|
136
|
+
wait_time=wait_time,
|
137
|
+
status=status,
|
138
|
+
waiting_message="Waiting after error...",
|
139
|
+
restore_message="Waiting for AI response...",
|
140
|
+
)
|
141
|
+
else:
|
142
|
+
_log_and_sleep(
|
143
|
+
"Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
|
144
|
+
attempt,
|
145
|
+
max_retries,
|
146
|
+
wait_time=wait_time,
|
147
|
+
)
|
148
|
+
return None
|
149
|
+
else:
|
150
|
+
print(tr("Max retries for invalid response reached. Raising error."))
|
151
|
+
raise e
|
152
|
+
|
153
|
+
|
154
|
+
def _handle_no_tool_support(error_message):
|
155
|
+
if "No endpoints found that support tool use" in error_message:
|
156
|
+
print(tr("API does not support tool use."))
|
157
|
+
raise NoToolSupportError(error_message)
|
158
|
+
|
159
|
+
|
160
|
+
def _handle_rate_limit(e, attempt, max_retries, status, status_code, retry_after):
|
161
|
+
wait_time = _calculate_wait_time(status_code, retry_after, attempt)
|
162
|
+
if attempt < max_retries:
|
163
|
+
if status is not None:
|
164
|
+
_log_and_sleep(
|
165
|
+
"OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
166
|
+
attempt,
|
167
|
+
max_retries,
|
168
|
+
e=e,
|
169
|
+
wait_time=wait_time,
|
170
|
+
status=status,
|
171
|
+
waiting_message="Waiting after rate limit reached...",
|
172
|
+
restore_message="Waiting for AI response...",
|
173
|
+
)
|
174
|
+
else:
|
175
|
+
_log_and_sleep(
|
176
|
+
"OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
177
|
+
attempt,
|
178
|
+
max_retries,
|
179
|
+
e=e,
|
180
|
+
wait_time=wait_time,
|
181
|
+
)
|
182
|
+
return None
|
183
|
+
else:
|
184
|
+
raise e
|
185
|
+
|
186
|
+
|
187
|
+
def _handle_server_error(e, attempt, max_retries, status, status_code):
|
188
|
+
wait_time = 2**attempt
|
189
|
+
if attempt < max_retries:
|
190
|
+
if status is not None:
|
191
|
+
_log_and_sleep(
|
192
|
+
"OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
193
|
+
attempt,
|
194
|
+
max_retries,
|
195
|
+
e=e,
|
196
|
+
wait_time=wait_time,
|
197
|
+
status=status,
|
198
|
+
waiting_message="Waiting after server error...",
|
199
|
+
restore_message="Waiting for AI response...",
|
200
|
+
)
|
201
|
+
else:
|
202
|
+
_log_and_sleep(
|
203
|
+
"OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
204
|
+
attempt,
|
205
|
+
max_retries,
|
206
|
+
e=e,
|
207
|
+
wait_time=wait_time,
|
208
|
+
)
|
209
|
+
return None
|
210
|
+
else:
|
211
|
+
print("Max retries for OpenAI API server error reached. Raising error.")
|
212
|
+
raise e
|
213
|
+
|
214
|
+
|
215
|
+
def _handle_client_error(e, status_code):
|
216
|
+
print(
|
217
|
+
tr(
|
218
|
+
"OpenAI API client error {status_code}: {e}. Not retrying.",
|
219
|
+
status_code=status_code,
|
220
|
+
e=e,
|
221
|
+
)
|
222
|
+
)
|
223
|
+
raise e
|
224
|
+
|
225
|
+
|
226
|
+
def _handle_generic_error(e, attempt, max_retries, status):
|
227
|
+
wait_time = 2**attempt
|
228
|
+
if attempt < max_retries:
|
229
|
+
if status is not None:
|
230
|
+
_log_and_sleep(
|
231
|
+
"OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
232
|
+
attempt,
|
233
|
+
max_retries,
|
234
|
+
e=e,
|
235
|
+
wait_time=wait_time,
|
236
|
+
status=status,
|
237
|
+
waiting_message="Waiting after error...",
|
238
|
+
restore_message="Waiting for AI response...",
|
239
|
+
)
|
240
|
+
else:
|
241
|
+
_log_and_sleep(
|
242
|
+
"OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
|
243
|
+
attempt,
|
244
|
+
max_retries,
|
245
|
+
e=e,
|
246
|
+
wait_time=wait_time,
|
247
|
+
)
|
248
|
+
print(f"[DEBUG] Exception repr: {repr(e)}")
|
249
|
+
return None
|
250
|
+
else:
|
251
|
+
print(tr("Max retries for OpenAI API error reached. Raising error."))
|
252
|
+
raise e
|
253
|
+
|
254
|
+
|
255
|
+
def _handle_general_exception(e, attempt, max_retries, status=None):
|
256
|
+
error_message = str(e)
|
257
|
+
_handle_no_tool_support(error_message)
|
258
|
+
status_code, retry_after = _extract_status_and_retry_after(e, error_message)
|
259
|
+
if status_code is not None:
|
260
|
+
if status_code == 429:
|
261
|
+
return _handle_rate_limit(
|
262
|
+
e, attempt, max_retries, status, status_code, retry_after
|
263
|
+
)
|
264
|
+
elif 500 <= status_code < 600:
|
265
|
+
return _handle_server_error(e, attempt, max_retries, status, status_code)
|
266
|
+
elif 400 <= status_code < 500:
|
267
|
+
_handle_client_error(e, status_code)
|
268
|
+
return _handle_generic_error(e, attempt, max_retries, status)
|
269
|
+
|
270
|
+
|
271
|
+
def retry_api_call(
|
272
|
+
api_func,
|
273
|
+
max_retries=5,
|
274
|
+
*args,
|
275
|
+
history=None,
|
276
|
+
user_message_on_empty=None,
|
277
|
+
status=None,
|
278
|
+
**kwargs,
|
279
|
+
):
|
280
|
+
for attempt in range(1, max_retries + 1):
|
281
|
+
try:
|
282
|
+
response = api_func(*args, **kwargs)
|
283
|
+
error = getattr(response, "error", None)
|
284
|
+
if error:
|
285
|
+
print(f"ApiError: {error.get('message', error)}")
|
286
|
+
raise ApiError(error.get("message", str(error)))
|
287
|
+
return response
|
288
|
+
except ApiError:
|
289
|
+
raise
|
290
|
+
except EmptyResponseError:
|
291
|
+
if history is not None and user_message_on_empty is not None:
|
292
|
+
print(
|
293
|
+
f"[DEBUG] Adding user message to history: {user_message_on_empty}"
|
294
|
+
)
|
295
|
+
history.add_message({"role": "user", "content": user_message_on_empty})
|
296
|
+
continue # Retry with updated history
|
297
|
+
else:
|
298
|
+
raise
|
299
|
+
except json.JSONDecodeError as e:
|
300
|
+
result = _handle_json_decode_error(e, attempt, max_retries, status=status)
|
301
|
+
if result is not None:
|
302
|
+
return result
|
303
|
+
except Exception as e:
|
304
|
+
result = _handle_general_exception(e, attempt, max_retries, status=status)
|
305
|
+
if result is not None:
|
306
|
+
return result
|
@@ -8,7 +8,7 @@ from rich.console import Console
|
|
8
8
|
def show_spinner(message, func, *args, **kwargs):
|
9
9
|
console = Console()
|
10
10
|
with console.status(message, spinner="dots") as status:
|
11
|
-
result = func(*args, **kwargs)
|
11
|
+
result = func(*args, status=status, **kwargs)
|
12
12
|
status.stop()
|
13
13
|
return result
|
14
14
|
|
@@ -68,3 +68,15 @@ class LLMConversationHistory:
|
|
68
68
|
|
69
69
|
def __getitem__(self, idx):
|
70
70
|
return self._messages[idx]
|
71
|
+
|
72
|
+
def remove_last_message(self):
|
73
|
+
"""Remove and return the last message in the history, or None if empty."""
|
74
|
+
if self._messages:
|
75
|
+
return self._messages.pop()
|
76
|
+
return None
|
77
|
+
|
78
|
+
def last_message(self):
|
79
|
+
"""Return the last message in the history, or None if empty."""
|
80
|
+
if self._messages:
|
81
|
+
return self._messages[-1]
|
82
|
+
return None
|
@@ -0,0 +1,30 @@
|
|
1
|
+
{# General role setup
|
2
|
+
ex. "Search in code" -> Python Developer -> find(*.py) | Java Developer -> find(*.java)
|
3
|
+
#}
|
4
|
+
You are: {{ role }}
|
5
|
+
|
6
|
+
{# Improves tool selection and platform specific constrains, eg, path format, C:\ vs /path #}
|
7
|
+
You will be developing and testing in the following environment:
|
8
|
+
Platform: {{ platform }}
|
9
|
+
Python version: {{ python_version }}
|
10
|
+
Shell/Environment: {{ shell_info }}
|
11
|
+
|
12
|
+
Respond according to the following guidelines:
|
13
|
+
{# Exploratory hint #}
|
14
|
+
- Before answering to the user, explore the content related to the question
|
15
|
+
{# Define exploration order, prefers search/outline, reduces chunking roundtip #}
|
16
|
+
- When exploring full files content, provide empty range to read the entire files instead of chunked reads
|
17
|
+
{# Prefix tools with purpose for user awarnesses #}
|
18
|
+
- Before using your namespace functions, provide a concise explanation.
|
19
|
+
{# Reduce unrequest code verbosity overhead #}
|
20
|
+
- Use the namespace functions to deliver the code changes instead of showing the code.
|
21
|
+
{# Drive edit mode, place holders critical as shown to be crucial to avoid corruption with code placeholders #}
|
22
|
+
- Prefer making localized edits using string replacements. If the required change is extensive, replace the entire file instead, provide full content without placeholders.
|
23
|
+
{# Trying to prevent surrogates generation, found this frequently in gpt4.1/windows #}
|
24
|
+
- While writing code, if you need an emoji or special Unicode character in a string, then insert the actual character (e.g., 📖) directly instead of using surrogate pairs or escape sequences.
|
25
|
+
{# Without this, the LLM choses to create files from a literal interpretation of the purpose and intention #}
|
26
|
+
- Before creating files search the code for the location related to the file purpose
|
27
|
+
{# This will trigger a search for the old names/locations to be updates #}
|
28
|
+
- After moving, removing or renaming functions or classes to different modules, update all imports, references, tests, and documentation to reflect the new locations, then verify functionality.
|
29
|
+
{# Keeping docstrings update is key to have semanatic match between prompts and code #}
|
30
|
+
- Once development or updates are finished, ensure that new or updated packages, modules, functions are properly documented.
|
@@ -3,6 +3,7 @@ from . import create_directory
|
|
3
3
|
from . import create_file
|
4
4
|
from . import replace_file
|
5
5
|
from . import fetch_url
|
6
|
+
from . import open_url
|
6
7
|
from . import find_files
|
7
8
|
from . import get_lines
|
8
9
|
from .get_file_outline import core # noqa: F401,F811
|
@@ -25,6 +26,7 @@ __all__ = [
|
|
25
26
|
"create_directory",
|
26
27
|
"create_file",
|
27
28
|
"fetch_url",
|
29
|
+
"open_url",
|
28
30
|
"find_files",
|
29
31
|
"GetFileOutlineTool",
|
30
32
|
"get_lines",
|
@@ -26,7 +26,7 @@ class CreateDirectoryTool(ToolBase):
|
|
26
26
|
disp_path = display_path(file_path)
|
27
27
|
self.report_info(
|
28
28
|
ActionType.WRITE,
|
29
|
-
tr("📁
|
29
|
+
tr("📁 Create directory '{disp_path}' ...", disp_path=disp_path),
|
30
30
|
)
|
31
31
|
try:
|
32
32
|
if os.path.exists(file_path):
|
@@ -45,7 +45,7 @@ class CreateFileTool(ToolBase):
|
|
45
45
|
os.makedirs(dir_name, exist_ok=True)
|
46
46
|
self.report_info(
|
47
47
|
ActionType.WRITE,
|
48
|
-
tr("📝
|
48
|
+
tr("📝 Create file '{disp_path}' ...", disp_path=disp_path),
|
49
49
|
)
|
50
50
|
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
51
51
|
f.write(content)
|
@@ -25,7 +25,7 @@ class FetchUrlTool(ToolBase):
|
|
25
25
|
if not url.strip():
|
26
26
|
self.report_warning(tr("ℹ️ Empty URL provided."))
|
27
27
|
return tr("Warning: Empty URL provided. Operation skipped.")
|
28
|
-
self.report_info(ActionType.READ, tr("🌐
|
28
|
+
self.report_info(ActionType.READ, tr("🌐 Fetch URL '{url}' ...", url=url))
|
29
29
|
try:
|
30
30
|
response = requests.get(url, timeout=10)
|
31
31
|
response.raise_for_status()
|
@@ -25,6 +25,26 @@ class FindFilesTool(ToolBase):
|
|
25
25
|
If max_results is reached, appends a note to the output.
|
26
26
|
"""
|
27
27
|
|
28
|
+
def _match_directories(self, root, dirs, pat):
|
29
|
+
dir_output = set()
|
30
|
+
dir_pat = pat.rstrip("/\\")
|
31
|
+
for d in dirs:
|
32
|
+
if fnmatch.fnmatch(d, dir_pat):
|
33
|
+
dir_output.add(os.path.join(root, d) + os.sep)
|
34
|
+
return dir_output
|
35
|
+
|
36
|
+
def _match_files(self, root, files, pat):
|
37
|
+
file_output = set()
|
38
|
+
for filename in fnmatch.filter(files, pat):
|
39
|
+
file_output.add(os.path.join(root, filename))
|
40
|
+
return file_output
|
41
|
+
|
42
|
+
def _match_dirs_without_slash(self, root, dirs, pat):
|
43
|
+
dir_output = set()
|
44
|
+
for d in fnmatch.filter(dirs, pat):
|
45
|
+
dir_output.add(os.path.join(root, d))
|
46
|
+
return dir_output
|
47
|
+
|
28
48
|
def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
|
29
49
|
if not pattern:
|
30
50
|
self.report_warning(tr("ℹ️ Empty file pattern provided."))
|
@@ -41,7 +61,7 @@ class FindFilesTool(ToolBase):
|
|
41
61
|
self.report_info(
|
42
62
|
ActionType.READ,
|
43
63
|
tr(
|
44
|
-
"🔍
|
64
|
+
"🔍 Search for files '{pattern}' in '{disp_path}'{depth_msg} ...",
|
45
65
|
pattern=pattern,
|
46
66
|
disp_path=disp_path,
|
47
67
|
depth_msg=depth_msg,
|
@@ -52,19 +72,13 @@ class FindFilesTool(ToolBase):
|
|
52
72
|
directory, max_depth=max_depth
|
53
73
|
):
|
54
74
|
for pat in patterns:
|
55
|
-
# Directory matching: pattern ends with '/' or '\'
|
56
75
|
if pat.endswith("/") or pat.endswith("\\"):
|
57
|
-
|
58
|
-
for d in dirs:
|
59
|
-
if fnmatch.fnmatch(d, dir_pat):
|
60
|
-
dir_output.add(os.path.join(root, d) + os.sep)
|
76
|
+
dir_output.update(self._match_directories(root, dirs, pat))
|
61
77
|
else:
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
for d in fnmatch.filter(dirs, pat):
|
67
|
-
dir_output.add(os.path.join(root, d))
|
78
|
+
dir_output.update(self._match_files(root, files, pat))
|
79
|
+
dir_output.update(
|
80
|
+
self._match_dirs_without_slash(root, dirs, pat)
|
81
|
+
)
|
68
82
|
self.report_success(
|
69
83
|
tr(
|
70
84
|
" ✅ {count} {file_word}",
|
@@ -72,7 +86,6 @@ class FindFilesTool(ToolBase):
|
|
72
86
|
file_word=pluralize("file", len(dir_output)),
|
73
87
|
)
|
74
88
|
)
|
75
|
-
# If searching in '.', strip leading './' from results
|
76
89
|
if directory.strip() == ".":
|
77
90
|
dir_output = {
|
78
91
|
p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
|
@@ -0,0 +1,178 @@
|
|
1
|
+
import re
|
2
|
+
from typing import List
|
3
|
+
|
4
|
+
|
5
|
+
def handle_assignment(idx, assign_match, outline):
|
6
|
+
var_name = assign_match.group(2)
|
7
|
+
var_type = "const" if var_name.isupper() else "var"
|
8
|
+
outline.append(
|
9
|
+
{
|
10
|
+
"type": var_type,
|
11
|
+
"name": var_name,
|
12
|
+
"start": idx + 1,
|
13
|
+
"end": idx + 1,
|
14
|
+
"parent": "",
|
15
|
+
"docstring": "",
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def handle_main(idx, outline):
|
21
|
+
outline.append(
|
22
|
+
{
|
23
|
+
"type": "main",
|
24
|
+
"name": "__main__",
|
25
|
+
"start": idx + 1,
|
26
|
+
"end": idx + 1,
|
27
|
+
"parent": "",
|
28
|
+
"docstring": "",
|
29
|
+
}
|
30
|
+
)
|
31
|
+
|
32
|
+
|
33
|
+
def close_stack_objects(idx, indent, stack, obj_ranges):
|
34
|
+
while stack and indent < stack[-1][2]:
|
35
|
+
popped = stack.pop()
|
36
|
+
obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
|
37
|
+
|
38
|
+
|
39
|
+
def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
|
40
|
+
if last_top_obj and last_top_obj in stack:
|
41
|
+
stack.remove(last_top_obj)
|
42
|
+
obj_ranges.append(
|
43
|
+
(
|
44
|
+
last_top_obj[0],
|
45
|
+
last_top_obj[1],
|
46
|
+
last_top_obj[3],
|
47
|
+
idx,
|
48
|
+
last_top_obj[4],
|
49
|
+
last_top_obj[2],
|
50
|
+
)
|
51
|
+
)
|
52
|
+
return None
|
53
|
+
return last_top_obj
|
54
|
+
|
55
|
+
|
56
|
+
def handle_class(idx, class_match, indent, stack, last_top_obj):
|
57
|
+
name = class_match.group(2)
|
58
|
+
parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
|
59
|
+
obj = ("class", name, indent, idx + 1, parent)
|
60
|
+
stack.append(obj)
|
61
|
+
if indent == 0:
|
62
|
+
last_top_obj = obj
|
63
|
+
return last_top_obj
|
64
|
+
|
65
|
+
|
66
|
+
def handle_function(idx, func_match, indent, stack, last_top_obj):
|
67
|
+
name = func_match.group(2)
|
68
|
+
parent = ""
|
69
|
+
for s in reversed(stack):
|
70
|
+
if s[0] == "class" and indent > s[2]:
|
71
|
+
parent = s[1]
|
72
|
+
break
|
73
|
+
obj = ("function", name, indent, idx + 1, parent)
|
74
|
+
stack.append(obj)
|
75
|
+
if indent == 0:
|
76
|
+
last_top_obj = obj
|
77
|
+
return last_top_obj
|
78
|
+
|
79
|
+
|
80
|
+
def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
|
81
|
+
class_pat, func_pat, assign_pat, main_pat = regexes
|
82
|
+
class_match = class_pat.match(line)
|
83
|
+
func_match = func_pat.match(line)
|
84
|
+
assign_match = assign_pat.match(line)
|
85
|
+
indent = len(line) - len(line.lstrip())
|
86
|
+
# If a new top-level class or function starts, close the previous one
|
87
|
+
if (class_match or func_match) and indent == 0 and last_top_obj:
|
88
|
+
last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
|
89
|
+
if class_match:
|
90
|
+
last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
|
91
|
+
elif func_match:
|
92
|
+
last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
|
93
|
+
elif assign_match and indent == 0:
|
94
|
+
handle_assignment(idx, assign_match, outline)
|
95
|
+
main_match = main_pat.match(line)
|
96
|
+
if main_match:
|
97
|
+
handle_main(idx, outline)
|
98
|
+
close_stack_objects(idx, indent, stack, obj_ranges)
|
99
|
+
return last_top_obj
|
100
|
+
|
101
|
+
|
102
|
+
def build_outline_entry(obj, lines, outline):
|
103
|
+
obj_type, name, start, end, parent, indent = obj
|
104
|
+
# Determine if this is a method
|
105
|
+
if obj_type == "function" and parent:
|
106
|
+
outline_type = "method"
|
107
|
+
elif obj_type == "function":
|
108
|
+
outline_type = "function"
|
109
|
+
else:
|
110
|
+
outline_type = obj_type
|
111
|
+
docstring = extract_docstring(lines, start, end)
|
112
|
+
outline.append(
|
113
|
+
{
|
114
|
+
"type": outline_type,
|
115
|
+
"name": name,
|
116
|
+
"start": start,
|
117
|
+
"end": end,
|
118
|
+
"parent": parent,
|
119
|
+
"docstring": docstring,
|
120
|
+
}
|
121
|
+
)
|
122
|
+
|
123
|
+
|
124
|
+
def process_lines(lines, regexes):
|
125
|
+
outline = []
|
126
|
+
stack = []
|
127
|
+
obj_ranges = []
|
128
|
+
last_top_obj = None
|
129
|
+
for idx, line in enumerate(lines):
|
130
|
+
last_top_obj = process_line(
|
131
|
+
idx, line, regexes, stack, obj_ranges, outline, last_top_obj
|
132
|
+
)
|
133
|
+
# Close any remaining open objects
|
134
|
+
for popped in stack:
|
135
|
+
obj_ranges.append(
|
136
|
+
(popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
|
137
|
+
)
|
138
|
+
return outline, obj_ranges
|
139
|
+
|
140
|
+
|
141
|
+
def build_outline(obj_ranges, lines, outline):
|
142
|
+
for obj in obj_ranges:
|
143
|
+
build_outline_entry(obj, lines, outline)
|
144
|
+
return outline
|
145
|
+
|
146
|
+
|
147
|
+
def parse_python_outline(lines: List[str]):
|
148
|
+
class_pat = re.compile(r"^(\s*)class\s+(\w+)")
|
149
|
+
func_pat = re.compile(r"^(\s*)def\s+(\w+)")
|
150
|
+
assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
|
151
|
+
main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
|
152
|
+
regexes = (class_pat, func_pat, assign_pat, main_pat)
|
153
|
+
outline, obj_ranges = process_lines(lines, regexes)
|
154
|
+
return build_outline(obj_ranges, lines, outline)
|
155
|
+
|
156
|
+
|
157
|
+
def extract_docstring(lines, start_idx, end_idx):
|
158
|
+
"""Extracts a docstring from lines[start_idx:end_idx] if present."""
|
159
|
+
for i in range(start_idx, min(end_idx, len(lines))):
|
160
|
+
line = lines[i].lstrip()
|
161
|
+
if not line:
|
162
|
+
continue
|
163
|
+
if line.startswith('"""') or line.startswith("'''"):
|
164
|
+
quote = line[:3]
|
165
|
+
doc = line[3:]
|
166
|
+
if doc.strip().endswith(quote):
|
167
|
+
return doc.strip()[:-3].strip()
|
168
|
+
docstring_lines = [doc]
|
169
|
+
for j in range(i + 1, min(end_idx, len(lines))):
|
170
|
+
line = lines[j]
|
171
|
+
if line.strip().endswith(quote):
|
172
|
+
docstring_lines.append(line.strip()[:-3])
|
173
|
+
return "\n".join([d.strip() for d in docstring_lines]).strip()
|
174
|
+
docstring_lines.append(line)
|
175
|
+
break
|
176
|
+
else:
|
177
|
+
break
|
178
|
+
return ""
|