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.
Files changed (205) hide show
  1. {janito-1.10.0/janito.egg-info → janito-1.11.1}/PKG-INFO +1 -1
  2. janito-1.11.1/janito/__init__.py +1 -0
  3. janito-1.11.1/janito/agent/conversation_api.py +306 -0
  4. {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_ui.py +1 -1
  5. {janito-1.10.0 → janito-1.11.1}/janito/agent/llm_conversation_history.py +12 -0
  6. janito-1.11.1/janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +30 -0
  7. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/__init__.py +2 -0
  8. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/create_directory.py +1 -1
  9. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/create_file.py +1 -1
  10. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/fetch_url.py +1 -1
  11. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/find_files.py +26 -13
  12. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/core.py +1 -1
  13. janito-1.11.1/janito/agent/tools/get_file_outline/python_outline.py +178 -0
  14. janito-1.11.1/janito/agent/tools/get_lines.py +149 -0
  15. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/move_file.py +58 -32
  16. janito-1.11.1/janito/agent/tools/open_url.py +31 -0
  17. janito-1.11.1/janito/agent/tools/python_command_runner.py +149 -0
  18. janito-1.11.1/janito/agent/tools/python_file_runner.py +147 -0
  19. janito-1.11.1/janito/agent/tools/python_stdin_runner.py +153 -0
  20. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/remove_directory.py +1 -1
  21. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/remove_file.py +1 -1
  22. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/replace_file.py +2 -2
  23. janito-1.11.1/janito/agent/tools/replace_text_in_file.py +262 -0
  24. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/run_bash_command.py +1 -1
  25. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/run_powershell_command.py +4 -0
  26. janito-1.11.1/janito/agent/tools/search_text/__init__.py +1 -0
  27. janito-1.11.1/janito/agent/tools/search_text/core.py +176 -0
  28. janito-1.11.1/janito/agent/tools/search_text/match_lines.py +58 -0
  29. janito-1.11.1/janito/agent/tools/search_text/pattern_utils.py +65 -0
  30. janito-1.11.1/janito/agent/tools/search_text/traverse_directory.py +132 -0
  31. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/core.py +41 -30
  32. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  33. janito-1.11.1/janito/agent/tools/validate_file_syntax/markdown_validator.py +109 -0
  34. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/gitignore_utils.py +25 -2
  35. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/utils.py +7 -1
  36. janito-1.11.1/janito/cli/config_commands.py +211 -0
  37. {janito-1.10.0 → janito-1.11.1}/janito/shell/main.py +51 -8
  38. janito-1.11.1/janito/shell/session/config.py +109 -0
  39. {janito-1.10.0 → janito-1.11.1}/janito/shell/ui/interactive.py +97 -73
  40. janito-1.11.1/janito/termweb/static/editor.css +145 -0
  41. janito-1.10.0/janito/termweb/static/editor.css → janito-1.11.1/janito/termweb/static/editor.css.bak +30 -27
  42. janito-1.10.0/janito/termweb/static/editor.html.bak → janito-1.11.1/janito/termweb/static/editor.html +17 -11
  43. janito-1.10.0/janito/termweb/static/editor.html → janito-1.11.1/janito/termweb/static/editor.html.bak +11 -7
  44. janito-1.10.0/janito/termweb/static/editor.js.bak → janito-1.11.1/janito/termweb/static/editor.js +101 -65
  45. janito-1.10.0/janito/termweb/static/editor.js → janito-1.11.1/janito/termweb/static/editor.js.bak +90 -40
  46. janito-1.10.0/janito/termweb/static/index.html.bak → janito-1.11.1/janito/termweb/static/index.html +1 -2
  47. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.css +1 -22
  48. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.css.bak +6 -4
  49. janito-1.10.0/janito/termweb/static/termweb.js.bak → janito-1.11.1/janito/termweb/static/termweb.js +1 -8
  50. {janito-1.10.0 → janito-1.11.1/janito.egg-info}/PKG-INFO +1 -1
  51. {janito-1.10.0 → janito-1.11.1}/janito.egg-info/SOURCES.txt +6 -1
  52. {janito-1.10.0 → janito-1.11.1}/pyproject.toml +1 -1
  53. janito-1.10.0/janito/__init__.py +0 -1
  54. janito-1.10.0/janito/agent/conversation_api.py +0 -218
  55. janito-1.10.0/janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +0 -15
  56. janito-1.10.0/janito/agent/tools/get_file_outline/python_outline.py +0 -134
  57. janito-1.10.0/janito/agent/tools/get_lines.py +0 -120
  58. janito-1.10.0/janito/agent/tools/python_command_runner.py +0 -150
  59. janito-1.10.0/janito/agent/tools/python_file_runner.py +0 -148
  60. janito-1.10.0/janito/agent/tools/python_stdin_runner.py +0 -154
  61. janito-1.10.0/janito/agent/tools/replace_text_in_file.py +0 -218
  62. janito-1.10.0/janito/agent/tools/search_text.py +0 -254
  63. janito-1.10.0/janito/agent/tools/validate_file_syntax/markdown_validator.py +0 -66
  64. janito-1.10.0/janito/cli/config_commands.py +0 -208
  65. janito-1.10.0/janito/shell/session/config.py +0 -101
  66. janito-1.10.0/janito/termweb/static/editor.css.bak +0 -27
  67. {janito-1.10.0 → janito-1.11.1}/LICENSE +0 -0
  68. {janito-1.10.0 → janito-1.11.1}/MANIFEST.in +0 -0
  69. {janito-1.10.0 → janito-1.11.1}/README.md +0 -0
  70. {janito-1.10.0 → janito-1.11.1}/janito/__main__.py +0 -0
  71. {janito-1.10.0 → janito-1.11.1}/janito/agent/__init__.py +0 -0
  72. {janito-1.10.0 → janito-1.11.1}/janito/agent/api_exceptions.py +0 -0
  73. {janito-1.10.0 → janito-1.11.1}/janito/agent/config.py +0 -0
  74. {janito-1.10.0 → janito-1.11.1}/janito/agent/config_defaults.py +0 -0
  75. {janito-1.10.0 → janito-1.11.1}/janito/agent/config_utils.py +0 -0
  76. {janito-1.10.0 → janito-1.11.1}/janito/agent/content_handler.py +0 -0
  77. {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation.py +0 -0
  78. {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_exceptions.py +0 -0
  79. {janito-1.10.0 → janito-1.11.1}/janito/agent/conversation_tool_calls.py +0 -0
  80. {janito-1.10.0 → janito-1.11.1}/janito/agent/event.py +0 -0
  81. {janito-1.10.0 → janito-1.11.1}/janito/agent/event_dispatcher.py +0 -0
  82. {janito-1.10.0 → janito-1.11.1}/janito/agent/event_handler_protocol.py +0 -0
  83. {janito-1.10.0 → janito-1.11.1}/janito/agent/event_system.py +0 -0
  84. {janito-1.10.0 → janito-1.11.1}/janito/agent/message_handler.py +0 -0
  85. {janito-1.10.0 → janito-1.11.1}/janito/agent/message_handler_protocol.py +0 -0
  86. {janito-1.10.0 → janito-1.11.1}/janito/agent/openai_client.py +0 -0
  87. {janito-1.10.0 → janito-1.11.1}/janito/agent/openai_schema_generator.py +0 -0
  88. {janito-1.10.0 → janito-1.11.1}/janito/agent/platform_discovery.py +0 -0
  89. {janito-1.10.0 → janito-1.11.1}/janito/agent/profile_manager.py +0 -0
  90. {janito-1.10.0 → janito-1.11.1}/janito/agent/queued_message_handler.py +0 -0
  91. {janito-1.10.0 → janito-1.11.1}/janito/agent/rich_live.py +0 -0
  92. {janito-1.10.0 → janito-1.11.1}/janito/agent/rich_message_handler.py +0 -0
  93. {janito-1.10.0 → janito-1.11.1}/janito/agent/runtime_config.py +0 -0
  94. {janito-1.10.0 → janito-1.11.1}/janito/agent/templates/profiles/system_prompt_template_base_pt.txt.j2 +0 -0
  95. {janito-1.10.0 → janito-1.11.1}/janito/agent/test_handler_protocols.py +0 -0
  96. {janito-1.10.0 → janito-1.11.1}/janito/agent/test_openai_schema_generator.py +0 -0
  97. {janito-1.10.0 → janito-1.11.1}/janito/agent/tests/__init__.py +0 -0
  98. {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_base.py +0 -0
  99. {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_executor.py +0 -0
  100. {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_registry.py +0 -0
  101. {janito-1.10.0 → janito-1.11.1}/janito/agent/tool_use_tracker.py +0 -0
  102. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/ask_user.py +0 -0
  103. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/delete_text_in_file.py +0 -0
  104. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/__init__.py +0 -0
  105. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/markdown_outline.py +0 -0
  106. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/get_file_outline/search_outline.py +0 -0
  107. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/present_choices.py +0 -0
  108. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/__init__.py +0 -0
  109. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/css_validator.py +0 -0
  110. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/js_validator.py +0 -0
  111. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/json_validator.py +0 -0
  112. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/ps1_validator.py +0 -0
  113. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/python_validator.py +0 -0
  114. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/xml_validator.py +0 -0
  115. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools/validate_file_syntax/yaml_validator.py +0 -0
  116. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/__init__.py +0 -0
  117. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/action_type.py +0 -0
  118. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/dir_walk_utils.py +0 -0
  119. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/formatting.py +0 -0
  120. {janito-1.10.0 → janito-1.11.1}/janito/agent/tools_utils/test_gitignore_utils.py +0 -0
  121. {janito-1.10.0 → janito-1.11.1}/janito/cli/__init__.py +0 -0
  122. {janito-1.10.0 → janito-1.11.1}/janito/cli/_livereload_log_utils.py +0 -0
  123. {janito-1.10.0 → janito-1.11.1}/janito/cli/_print_config.py +0 -0
  124. {janito-1.10.0 → janito-1.11.1}/janito/cli/_termweb_log_utils.py +0 -0
  125. {janito-1.10.0 → janito-1.11.1}/janito/cli/_utils.py +0 -0
  126. {janito-1.10.0 → janito-1.11.1}/janito/cli/arg_parser.py +0 -0
  127. {janito-1.10.0 → janito-1.11.1}/janito/cli/cli_main.py +0 -0
  128. {janito-1.10.0 → janito-1.11.1}/janito/cli/config_runner.py +0 -0
  129. {janito-1.10.0 → janito-1.11.1}/janito/cli/formatting_runner.py +0 -0
  130. {janito-1.10.0 → janito-1.11.1}/janito/cli/livereload_starter.py +0 -0
  131. {janito-1.10.0 → janito-1.11.1}/janito/cli/logging_setup.py +0 -0
  132. {janito-1.10.0 → janito-1.11.1}/janito/cli/main.py +0 -0
  133. {janito-1.10.0 → janito-1.11.1}/janito/cli/one_shot.py +0 -0
  134. {janito-1.10.0 → janito-1.11.1}/janito/cli/termweb_starter.py +0 -0
  135. {janito-1.10.0 → janito-1.11.1}/janito/i18n/__init__.py +0 -0
  136. {janito-1.10.0 → janito-1.11.1}/janito/i18n/messages.py +0 -0
  137. {janito-1.10.0 → janito-1.11.1}/janito/i18n/pt.py +0 -0
  138. {janito-1.10.0 → janito-1.11.1}/janito/livereload/app.py +0 -0
  139. {janito-1.10.0 → janito-1.11.1}/janito/rich_utils.py +0 -0
  140. {janito-1.10.0 → janito-1.11.1}/janito/shell/__init__.py +0 -0
  141. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/__init__.py +0 -0
  142. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/config.py +0 -0
  143. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/conversation_restart.py +0 -0
  144. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/edit.py +0 -0
  145. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/history_view.py +0 -0
  146. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/lang.py +0 -0
  147. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/livelogs.py +0 -0
  148. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/prompt.py +0 -0
  149. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/session.py +0 -0
  150. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/session_control.py +0 -0
  151. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/termweb_log.py +0 -0
  152. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/tools.py +0 -0
  153. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/track.py +0 -0
  154. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/utility.py +0 -0
  155. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands/verbose.py +0 -0
  156. {janito-1.10.0 → janito-1.11.1}/janito/shell/commands.py +0 -0
  157. {janito-1.10.0 → janito-1.11.1}/janito/shell/input_history.py +0 -0
  158. {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/completer.py +0 -0
  159. {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/load_prompt.py +0 -0
  160. {janito-1.10.0 → janito-1.11.1}/janito/shell/prompt/session_setup.py +0 -0
  161. {janito-1.10.0 → janito-1.11.1}/janito/shell/session/history.py +0 -0
  162. {janito-1.10.0 → janito-1.11.1}/janito/shell/session/manager.py +0 -0
  163. {janito-1.10.0 → janito-1.11.1}/janito/termweb/app.py +0 -0
  164. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/explorer.html.bak +0 -0
  165. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/favicon.ico +0 -0
  166. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/favicon.ico.bak +0 -0
  167. /janito-1.10.0/janito/termweb/static/index.html → /janito-1.11.1/janito/termweb/static/index.html.bak +0 -0
  168. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/index.html.bak.bak +0 -0
  169. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/landing.html.bak +0 -0
  170. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termicon.svg +0 -0
  171. /janito-1.10.0/janito/termweb/static/termweb.js → /janito-1.11.1/janito/termweb/static/termweb.js.bak +0 -0
  172. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb.js.bak.bak +0 -0
  173. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb_quickopen.js +0 -0
  174. {janito-1.10.0 → janito-1.11.1}/janito/termweb/static/termweb_quickopen.js.bak +0 -0
  175. {janito-1.10.0 → janito-1.11.1}/janito/tests/test_rich_utils.py +0 -0
  176. {janito-1.10.0 → janito-1.11.1}/janito/web/__init__.py +0 -0
  177. {janito-1.10.0 → janito-1.11.1}/janito/web/__main__.py +0 -0
  178. {janito-1.10.0 → janito-1.11.1}/janito/web/app.py +0 -0
  179. {janito-1.10.0 → janito-1.11.1}/janito.egg-info/dependency_links.txt +0 -0
  180. {janito-1.10.0 → janito-1.11.1}/janito.egg-info/entry_points.txt +0 -0
  181. {janito-1.10.0 → janito-1.11.1}/janito.egg-info/requires.txt +0 -0
  182. {janito-1.10.0 → janito-1.11.1}/janito.egg-info/top_level.txt +0 -0
  183. {janito-1.10.0 → janito-1.11.1}/setup.cfg +0 -0
  184. {janito-1.10.0 → janito-1.11.1}/tests/test_basic.py +0 -0
  185. {janito-1.10.0 → janito-1.11.1}/tests/test_find_files.py +0 -0
  186. {janito-1.10.0 → janito-1.11.1}/tests/test_outline_formatter.py +0 -0
  187. {janito-1.10.0 → janito-1.11.1}/tests/test_outline_no_overlap.py +0 -0
  188. {janito-1.10.0 → janito-1.11.1}/tests/test_outline_python_file.py +0 -0
  189. {janito-1.10.0 → janito-1.11.1}/tests/test_outline_python_file_complex.py +0 -0
  190. {janito-1.10.0 → janito-1.11.1}/tests/test_outline_tool_run.py +0 -0
  191. {janito-1.10.0 → janito-1.11.1}/tests/test_platform_discovery.py +0 -0
  192. {janito-1.10.0 → janito-1.11.1}/tests/test_python_command_runner.py +0 -0
  193. {janito-1.10.0 → janito-1.11.1}/tests/test_python_file_runner.py +0 -0
  194. {janito-1.10.0 → janito-1.11.1}/tests/test_python_stdin_runner.py +0 -0
  195. {janito-1.10.0 → janito-1.11.1}/tests/test_rich_message_handler_action_type.py +0 -0
  196. {janito-1.10.0 → janito-1.11.1}/tests/test_run_powershell_command.py +0 -0
  197. {janito-1.10.0 → janito-1.11.1}/tests/test_search_text.py +0 -0
  198. {janito-1.10.0 → janito-1.11.1}/tests/test_set_role.py +0 -0
  199. {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_docstring_formats.py +0 -0
  200. {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_manual_sim.py +0 -0
  201. {janito-1.10.0 → janito-1.11.1}/tests/test_tool_registry_validation.py +0 -0
  202. {janito-1.10.0 → janito-1.11.1}/tests/test_tool_use_tracker.py +0 -0
  203. {janito-1.10.0 → janito-1.11.1}/tests/test_validate_file_syntax.py +0 -0
  204. {janito-1.10.0 → janito-1.11.1}/tests/test_validate_file_syntax_xml_html.py +0 -0
  205. {janito-1.10.0 → janito-1.11.1}/tests/test_validate_markdown_syntax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janito
3
- Version: 1.10.0
3
+ Version: 1.11.1
4
4
  Summary: Natural Language Coding Agent,
5
5
  Author-email: João Pinto <joao.pinto@gmail.com>
6
6
  License-Expression: MIT
@@ -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("📁 Creating directory '{disp_path}' ...", disp_path=disp_path),
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("📝 Creating file '{disp_path}' ...", disp_path=disp_path),
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("🌐 Fetching URL '{url}' ...", url=url))
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
- "🔍 Searching for files '{pattern}' in '{disp_path}'{depth_msg} ...",
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
- dir_pat = pat.rstrip("/\\")
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
- # Match files
63
- for filename in fnmatch.filter(files, pat):
64
- dir_output.add(os.path.join(root, filename))
65
- # Also match directories (without trailing slash)
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
@@ -23,7 +23,7 @@ class GetFileOutlineTool(ToolBase):
23
23
  self.report_info(
24
24
  ActionType.READ,
25
25
  tr(
26
- "📄 Outlining file '{disp_path}' ...",
26
+ "📄 Outline file '{disp_path}' ...",
27
27
  disp_path=display_path(file_path),
28
28
  ),
29
29
  )
@@ -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 ""