xai-review 0.10.0__tar.gz → 0.12.0__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.

Potentially problematic release.


This version of xai-review might be problematic. Click here for more details.

Files changed (173) hide show
  1. {xai_review-0.10.0 → xai_review-0.12.0}/PKG-INFO +1 -1
  2. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/prompt.py +1 -0
  3. xai_review-0.12.0/ai_review/libs/json.py +16 -0
  4. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/prompt/service.py +19 -18
  5. xai_review-0.12.0/ai_review/services/prompt/tools.py +24 -0
  6. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/inline/service.py +2 -2
  7. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/test_json.py +2 -16
  8. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/prompt/test_service.py +33 -0
  9. xai_review-0.12.0/ai_review/tests/suites/services/prompt/test_tools.py +72 -0
  10. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/inline/test_service.py +6 -0
  11. {xai_review-0.10.0 → xai_review-0.12.0}/pyproject.toml +1 -1
  12. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/PKG-INFO +1 -1
  13. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/SOURCES.txt +2 -0
  14. xai_review-0.10.0/ai_review/libs/json.py +0 -19
  15. {xai_review-0.10.0 → xai_review-0.12.0}/LICENSE +0 -0
  16. {xai_review-0.10.0 → xai_review-0.12.0}/README.md +0 -0
  17. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/__init__.py +0 -0
  18. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/__init__.py +0 -0
  19. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/commands/__init__.py +0 -0
  20. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/commands/run_context_review.py +0 -0
  21. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/commands/run_inline_review.py +0 -0
  22. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/commands/run_review.py +0 -0
  23. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/commands/run_summary_review.py +0 -0
  24. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/cli/main.py +0 -0
  25. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/__init__.py +0 -0
  26. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/claude/__init__.py +0 -0
  27. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/claude/client.py +0 -0
  28. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/claude/schema.py +0 -0
  29. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gemini/__init__.py +0 -0
  30. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gemini/client.py +0 -0
  31. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gemini/schema.py +0 -0
  32. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/__init__.py +0 -0
  33. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/client.py +0 -0
  34. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/__init__.py +0 -0
  35. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/client.py +0 -0
  36. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/schema/__init__.py +0 -0
  37. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/schema/changes.py +0 -0
  38. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/schema/comments.py +0 -0
  39. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/gitlab/mr/schema/discussions.py +0 -0
  40. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/openai/__init__.py +0 -0
  41. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/openai/client.py +0 -0
  42. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/clients/openai/schema.py +0 -0
  43. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/config.py +0 -0
  44. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/__init__.py +0 -0
  45. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/asynchronous/__init__.py +0 -0
  46. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/asynchronous/gather.py +0 -0
  47. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/__init__.py +0 -0
  48. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/artifacts.py +0 -0
  49. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/base.py +0 -0
  50. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/claude.py +0 -0
  51. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/gemini.py +0 -0
  52. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/gitlab.py +0 -0
  53. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/http.py +0 -0
  54. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/llm.py +0 -0
  55. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/logger.py +0 -0
  56. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/openai.py +0 -0
  57. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/review.py +0 -0
  58. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/config/vcs.py +0 -0
  59. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/constants/__init__.py +0 -0
  60. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/constants/llm_provider.py +0 -0
  61. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/constants/vcs_provider.py +0 -0
  62. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/diff/__init__.py +0 -0
  63. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/diff/models.py +0 -0
  64. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/diff/parser.py +0 -0
  65. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/diff/tools.py +0 -0
  66. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/__init__.py +0 -0
  67. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/client.py +0 -0
  68. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/event_hooks/__init__.py +0 -0
  69. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/event_hooks/base.py +0 -0
  70. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/event_hooks/logger.py +0 -0
  71. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/handlers.py +0 -0
  72. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/transports/__init__.py +0 -0
  73. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/http/transports/retry.py +0 -0
  74. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/logger.py +0 -0
  75. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/resources.py +0 -0
  76. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/template/__init__.py +0 -0
  77. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/libs/template/render.py +0 -0
  78. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/__init__.py +0 -0
  79. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_context.md +0 -0
  80. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_inline.md +0 -0
  81. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_summary.md +0 -0
  82. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_system_context.md +0 -0
  83. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_system_inline.md +0 -0
  84. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/prompts/default_system_summary.md +0 -0
  85. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/resources/__init__.py +0 -0
  86. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/resources/pricing.yaml +0 -0
  87. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/__init__.py +0 -0
  88. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/artifacts/__init__.py +0 -0
  89. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/artifacts/schema.py +0 -0
  90. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/artifacts/service.py +0 -0
  91. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/artifacts/tools.py +0 -0
  92. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/cost/__init__.py +0 -0
  93. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/cost/schema.py +0 -0
  94. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/cost/service.py +0 -0
  95. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/diff/__init__.py +0 -0
  96. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/diff/renderers.py +0 -0
  97. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/diff/schema.py +0 -0
  98. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/diff/service.py +0 -0
  99. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/diff/tools.py +0 -0
  100. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/git/__init__.py +0 -0
  101. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/git/service.py +0 -0
  102. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/git/types.py +0 -0
  103. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/__init__.py +0 -0
  104. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/claude/__init__.py +0 -0
  105. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/claude/client.py +0 -0
  106. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/factory.py +0 -0
  107. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/gemini/__init__.py +0 -0
  108. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/gemini/client.py +0 -0
  109. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/openai/__init__.py +0 -0
  110. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/openai/client.py +0 -0
  111. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/llm/types.py +0 -0
  112. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/prompt/__init__.py +0 -0
  113. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/prompt/adapter.py +0 -0
  114. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/prompt/schema.py +0 -0
  115. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/__init__.py +0 -0
  116. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/inline/__init__.py +0 -0
  117. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/inline/schema.py +0 -0
  118. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/policy/__init__.py +0 -0
  119. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/policy/service.py +0 -0
  120. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/service.py +0 -0
  121. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/summary/__init__.py +0 -0
  122. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/summary/schema.py +0 -0
  123. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/review/summary/service.py +0 -0
  124. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/vcs/__init__.py +0 -0
  125. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/vcs/factory.py +0 -0
  126. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/vcs/gitlab/__init__.py +0 -0
  127. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/vcs/gitlab/client.py +0 -0
  128. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/services/vcs/types.py +0 -0
  129. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/__init__.py +0 -0
  130. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/fixtures/__init__.py +0 -0
  131. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/fixtures/git.py +0 -0
  132. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/__init__.py +0 -0
  133. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/__init__.py +0 -0
  134. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/claude/__init__.py +0 -0
  135. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/claude/test_client.py +0 -0
  136. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/claude/test_schema.py +0 -0
  137. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/gemini/__init__.py +0 -0
  138. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/gemini/test_client.py +0 -0
  139. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/gemini/test_schema.py +0 -0
  140. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/gitlab/__init__.py +0 -0
  141. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/gitlab/test_client.py +0 -0
  142. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/openai/__init__.py +0 -0
  143. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/openai/test_client.py +0 -0
  144. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/clients/openai/test_schema.py +0 -0
  145. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/__init__.py +0 -0
  146. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/config/__init__.py +0 -0
  147. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/config/test_prompt.py +0 -0
  148. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/diff/__init__.py +0 -0
  149. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/diff/test_models.py +0 -0
  150. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/diff/test_parser.py +0 -0
  151. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/diff/test_tools.py +0 -0
  152. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/template/__init__.py +0 -0
  153. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/libs/template/test_render.py +0 -0
  154. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/__init__.py +0 -0
  155. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/diff/__init__.py +0 -0
  156. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/diff/test_renderers.py +0 -0
  157. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/diff/test_service.py +0 -0
  158. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/diff/test_tools.py +0 -0
  159. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/prompt/__init__.py +0 -0
  160. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/prompt/test_schema.py +0 -0
  161. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/__init__.py +0 -0
  162. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/inline/__init__.py +0 -0
  163. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/inline/test_schema.py +0 -0
  164. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/policy/__init__.py +0 -0
  165. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/policy/test_service.py +0 -0
  166. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/summary/__init__.py +0 -0
  167. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/summary/test_schema.py +0 -0
  168. {xai_review-0.10.0 → xai_review-0.12.0}/ai_review/tests/suites/services/review/summary/test_service.py +0 -0
  169. {xai_review-0.10.0 → xai_review-0.12.0}/setup.cfg +0 -0
  170. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/dependency_links.txt +0 -0
  171. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/entry_points.txt +0 -0
  172. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/requires.txt +0 -0
  173. {xai_review-0.10.0 → xai_review-0.12.0}/xai_review.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary: AI-powered code review tool
5
5
  Author-email: Nikita Filonov <nikita.filonov@example.com>
6
6
  Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
@@ -8,6 +8,7 @@ from ai_review.libs.resources import load_resource
8
8
 
9
9
  class PromptConfig(BaseModel):
10
10
  context: dict[str, str] = Field(default_factory=dict)
11
+ normalize_prompts: bool = True
11
12
  context_placeholder: str = "<<{value}>>"
12
13
  inline_prompt_files: list[FilePath] | None = None
13
14
  context_prompt_files: list[FilePath] | None = None
@@ -0,0 +1,16 @@
1
+ import re
2
+
3
+ CONTROL_CHARS_RE = re.compile(r"[\x00-\x1F]")
4
+ REPLACEMENTS = {
5
+ "\n": "\\n",
6
+ "\r": "\\r",
7
+ "\t": "\\t",
8
+ }
9
+
10
+
11
+ def sanitize_json_string(raw: str) -> str:
12
+ def replace(match: re.Match) -> str:
13
+ char = match.group()
14
+ return REPLACEMENTS.get(char, f"\\u{ord(char):04x}")
15
+
16
+ return CONTROL_CHARS_RE.sub(replace, raw)
@@ -1,56 +1,57 @@
1
1
  from ai_review.config import settings
2
2
  from ai_review.services.diff.schema import DiffFileSchema
3
3
  from ai_review.services.prompt.schema import PromptContextSchema
4
+ from ai_review.services.prompt.tools import normalize_prompt, format_file
4
5
 
5
6
 
6
- def format_file(diff: DiffFileSchema) -> str:
7
- return f"# File: {diff.file}\n{diff.diff}\n"
7
+ class PromptService:
8
+ @classmethod
9
+ def prepare_prompt(cls, prompts: list[str], context: PromptContextSchema) -> str:
10
+ prompt = "\n\n".join(prompts)
11
+ prompt = context.apply_format(prompt)
8
12
 
13
+ if settings.prompt.normalize_prompts:
14
+ prompt = normalize_prompt(prompt)
15
+
16
+ return prompt
9
17
 
10
- class PromptService:
11
18
  @classmethod
12
19
  def build_inline_request(cls, diff: DiffFileSchema, context: PromptContextSchema) -> str:
13
- inline_prompts = "\n\n".join(settings.prompt.load_inline())
14
- inline_prompts = context.apply_format(inline_prompts)
20
+ prompt = cls.prepare_prompt(settings.prompt.load_inline(), context)
15
21
  return (
16
- f"{inline_prompts}\n\n"
22
+ f"{prompt}\n\n"
17
23
  f"## Diff\n\n"
18
24
  f"{format_file(diff)}"
19
25
  )
20
26
 
21
27
  @classmethod
22
28
  def build_summary_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
29
+ prompt = cls.prepare_prompt(settings.prompt.load_summary(), context)
23
30
  changes = "\n\n".join(map(format_file, diffs))
24
- summary_prompts = "\n\n".join(settings.prompt.load_summary())
25
- summary_prompts = context.apply_format(summary_prompts)
26
31
  return (
27
- f"{summary_prompts}\n\n"
32
+ f"{prompt}\n\n"
28
33
  f"## Changes\n\n"
29
34
  f"{changes}\n"
30
35
  )
31
36
 
32
37
  @classmethod
33
38
  def build_context_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
39
+ prompt = cls.prepare_prompt(settings.prompt.load_context(), context)
34
40
  changes = "\n\n".join(map(format_file, diffs))
35
- inline_prompts = "\n\n".join(settings.prompt.load_context())
36
- inline_prompts = context.apply_format(inline_prompts)
37
41
  return (
38
- f"{inline_prompts}\n\n"
42
+ f"{prompt}\n\n"
39
43
  f"## Diff\n\n"
40
44
  f"{changes}\n"
41
45
  )
42
46
 
43
47
  @classmethod
44
48
  def build_system_inline_request(cls, context: PromptContextSchema) -> str:
45
- prompt = "\n\n".join(settings.prompt.load_system_inline())
46
- return context.apply_format(prompt)
49
+ return cls.prepare_prompt(settings.prompt.load_system_inline(), context)
47
50
 
48
51
  @classmethod
49
52
  def build_system_context_request(cls, context: PromptContextSchema) -> str:
50
- prompt = "\n\n".join(settings.prompt.load_system_context())
51
- return context.apply_format(prompt)
53
+ return cls.prepare_prompt(settings.prompt.load_system_context(), context)
52
54
 
53
55
  @classmethod
54
56
  def build_system_summary_request(cls, context: PromptContextSchema) -> str:
55
- prompt = "\n\n".join(settings.prompt.load_system_summary())
56
- return context.apply_format(prompt)
57
+ return cls.prepare_prompt(settings.prompt.load_system_summary(), context)
@@ -0,0 +1,24 @@
1
+ import re
2
+
3
+ from ai_review.libs.logger import get_logger
4
+ from ai_review.services.diff.schema import DiffFileSchema
5
+
6
+ logger = get_logger("PROMPT_TOOLS")
7
+
8
+
9
+ def format_file(diff: DiffFileSchema) -> str:
10
+ return f"# File: {diff.file}\n{diff.diff}\n"
11
+
12
+
13
+ def normalize_prompt(text: str) -> str:
14
+ tails_stripped = [re.sub(r"[ \t]+$", "", line) for line in text.splitlines()]
15
+ text = "\n".join(tails_stripped)
16
+
17
+ text = re.sub(r"\n{3,}", "\n\n", text)
18
+
19
+ result = text.strip()
20
+ if len(text) > len(result):
21
+ logger.info(f"Prompt has been normalized from {len(text)} to {len(result)}")
22
+ return result
23
+
24
+ return text
@@ -18,12 +18,12 @@ class InlineCommentService:
18
18
  try:
19
19
  return InlineCommentListSchema.model_validate_json(raw)
20
20
  except ValidationError as error:
21
- logger.debug(f"Parse failed, trying sanitized JSON: {raw[:200]=}, {error=}")
21
+ logger.warning(f"Parse failed, trying sanitized JSON: {raw[:200]=}, {error=}")
22
22
  try:
23
23
  cleaned = sanitize_json_string(raw)
24
24
  return InlineCommentListSchema.model_validate_json(cleaned)
25
25
  except ValidationError as error:
26
- logger.debug(f"Sanitized JSON still invalid: {raw[:200]=}, {error=}")
26
+ logger.warning(f"Sanitized JSON still invalid: {raw[:200]=}, {error=}")
27
27
  return None
28
28
 
29
29
  @classmethod
@@ -12,30 +12,16 @@ from ai_review.libs.json import sanitize_json_string
12
12
  ("a\tb", "a\\tb"),
13
13
  ("abc\0def", "abc\\u0000def"),
14
14
  ("x\n\ry\t\0z", "x\\n\\ry\\t\\u0000z"),
15
+ ("\n\r\t\0", "\\n\\r\\t\\u0000"),
16
+ ("", "")
15
17
  ],
16
18
  )
17
19
  def test_sanitize_basic_cases(actual: str, expected: str) -> None:
18
20
  assert sanitize_json_string(actual) == expected
19
21
 
20
22
 
21
- def test_sanitize_multiple_control_chars() -> None:
22
- raw = "A\nB\rC\tD\0E"
23
- result = sanitize_json_string(raw)
24
- assert result == "A\\nB\\rC\\tD\\u0000E"
25
-
26
-
27
23
  def test_sanitize_idempotent() -> None:
28
24
  raw = "foo\nbar"
29
25
  once = sanitize_json_string(raw)
30
26
  twice = sanitize_json_string(once)
31
27
  assert once == twice
32
-
33
-
34
- def test_sanitize_empty_string() -> None:
35
- assert sanitize_json_string("") == ""
36
-
37
-
38
- def test_sanitize_only_control_chars() -> None:
39
- raw = "\n\r\t\0"
40
- result = sanitize_json_string(raw)
41
- assert result == "\\n\\r\\t\\u0000"
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
 
3
+ from ai_review.config import settings
3
4
  from ai_review.libs.config.prompt import PromptConfig
4
5
  from ai_review.services.diff.schema import DiffFileSchema
5
6
  from ai_review.services.prompt.schema import PromptContextSchema
@@ -134,3 +135,35 @@ def test_diff_placeholders_are_not_replaced(dummy_context: PromptContextSchema)
134
135
 
135
136
  assert "<<merge_request_title>>" in result
136
137
  assert "Fix login bug" not in result
138
+
139
+
140
+ def test_prepare_prompt_basic_substitution(dummy_context: PromptContextSchema) -> None:
141
+ prompts = ["Hello", "MR title: <<merge_request_title>>"]
142
+ result = PromptService.prepare_prompt(prompts, dummy_context)
143
+ assert "Hello" in result
144
+ assert "MR title: Fix login bug" in result
145
+
146
+
147
+ def test_prepare_prompt_applies_normalization(
148
+ monkeypatch: pytest.MonkeyPatch,
149
+ dummy_context: PromptContextSchema
150
+ ) -> None:
151
+ monkeypatch.setattr(settings.prompt, "normalize_prompts", True)
152
+ prompts = ["Line with space ", "", "", "Next line"]
153
+ result = PromptService.prepare_prompt(prompts, dummy_context)
154
+
155
+ assert "Line with space" in result
156
+ assert "Next line" in result
157
+ assert "\n\n\n" not in result
158
+
159
+
160
+ def test_prepare_prompt_skips_normalization(
161
+ monkeypatch: pytest.MonkeyPatch,
162
+ dummy_context: PromptContextSchema
163
+ ) -> None:
164
+ monkeypatch.setattr(settings.prompt, "normalize_prompts", False)
165
+ prompts = ["Line with space ", "", "", "Next line"]
166
+ result = PromptService.prepare_prompt(prompts, dummy_context)
167
+
168
+ assert "Line with space " in result
169
+ assert "\n\n\n" in result
@@ -0,0 +1,72 @@
1
+ from ai_review.services.diff.schema import DiffFileSchema
2
+ from ai_review.services.prompt.tools import format_file, normalize_prompt
3
+
4
+
5
+ def test_format_file_basic():
6
+ diff = DiffFileSchema(file="main.py", diff="+ print('hello')")
7
+ result = format_file(diff)
8
+ assert result == "# File: main.py\n+ print('hello')\n"
9
+
10
+
11
+ def test_format_file_empty_diff():
12
+ diff = DiffFileSchema(file="empty.py", diff="")
13
+ result = format_file(diff)
14
+ assert result == "# File: empty.py\n\n"
15
+
16
+
17
+ def test_format_file_multiline_diff():
18
+ diff = DiffFileSchema(
19
+ file="utils/helpers.py",
20
+ diff="- old line\n+ new line\n+ another line"
21
+ )
22
+ result = format_file(diff)
23
+ expected = (
24
+ "# File: utils/helpers.py\n"
25
+ "- old line\n"
26
+ "+ new line\n"
27
+ "+ another line\n"
28
+ )
29
+ assert result == expected
30
+
31
+
32
+ def test_format_file_filename_with_path():
33
+ diff = DiffFileSchema(file="src/app/models/user.py", diff="+ class User:")
34
+ result = format_file(diff)
35
+ assert result.startswith("# File: src/app/models/user.py\n")
36
+ assert result.endswith("+ class User:\n")
37
+
38
+
39
+ def test_trailing_spaces_are_removed():
40
+ text = "hello \nworld\t\t"
41
+ result = normalize_prompt(text)
42
+ assert result == "hello\nworld"
43
+
44
+
45
+ def test_multiple_empty_lines_collapsed():
46
+ text = "line1\n\n\n\nline2"
47
+ result = normalize_prompt(text)
48
+ assert result == "line1\n\nline2"
49
+
50
+
51
+ def test_leading_and_trailing_whitespace_removed():
52
+ text = "\n\n hello\nworld \n\n"
53
+ result = normalize_prompt(text)
54
+ assert result == "hello\nworld"
55
+
56
+
57
+ def test_internal_spaces_preserved():
58
+ text = "foo bar\nbaz\t\tqux"
59
+ result = normalize_prompt(text)
60
+ assert result == "foo bar\nbaz\t\tqux"
61
+
62
+
63
+ def test_only_whitespace_string():
64
+ text = " \n \n"
65
+ result = normalize_prompt(text)
66
+ assert result == ""
67
+
68
+
69
+ def test_no_changes_when_already_clean():
70
+ text = "line1\nline2"
71
+ result = normalize_prompt(text)
72
+ assert result == text
@@ -53,6 +53,8 @@ def test_json_with_raw_newline_sanitized():
53
53
  output = '[{"file": "e.py", "line": 3, "message": "line1\nline2"}]'
54
54
  result = InlineCommentService.parse_model_output(output)
55
55
  assert len(result.root) == 1
56
+ assert result.root[0].file == "e.py"
57
+ assert result.root[0].line == 3
56
58
  assert result.root[0].message == "line1\nline2"
57
59
 
58
60
 
@@ -85,6 +87,8 @@ def test_try_parse_valid_json():
85
87
  assert isinstance(result, InlineCommentListSchema)
86
88
  assert len(result.root) == 1
87
89
  assert result.root[0].file == "ok.py"
90
+ assert result.root[0].line == 1
91
+ assert result.root[0].message == "all good"
88
92
 
89
93
 
90
94
  def test_try_parse_needs_sanitization():
@@ -92,6 +96,8 @@ def test_try_parse_needs_sanitization():
92
96
  result = InlineCommentService.try_parse_model_output(raw)
93
97
  assert result is not None
94
98
  assert result.root[0].file == "bad.py"
99
+ assert result.root[0].line == 2
100
+ assert result.root[0].message == "line1\nline2"
95
101
  assert "line1" in result.root[0].message
96
102
 
97
103
 
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "xai-review"
7
7
  readme = "README.md"
8
- version = "0.10.0"
8
+ version = "0.12.0"
9
9
  license = { text = "Apache-2.0" }
10
10
  authors = [
11
11
  { name = "Nikita Filonov", email = "nikita.filonov@example.com" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xai-review
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary: AI-powered code review tool
5
5
  Author-email: Nikita Filonov <nikita.filonov@example.com>
6
6
  Maintainer-email: Nikita Filonov <nikita.filonov@example.com>
@@ -102,6 +102,7 @@ ai_review/services/prompt/__init__.py
102
102
  ai_review/services/prompt/adapter.py
103
103
  ai_review/services/prompt/schema.py
104
104
  ai_review/services/prompt/service.py
105
+ ai_review/services/prompt/tools.py
105
106
  ai_review/services/review/__init__.py
106
107
  ai_review/services/review/service.py
107
108
  ai_review/services/review/inline/__init__.py
@@ -151,6 +152,7 @@ ai_review/tests/suites/services/diff/test_tools.py
151
152
  ai_review/tests/suites/services/prompt/__init__.py
152
153
  ai_review/tests/suites/services/prompt/test_schema.py
153
154
  ai_review/tests/suites/services/prompt/test_service.py
155
+ ai_review/tests/suites/services/prompt/test_tools.py
154
156
  ai_review/tests/suites/services/review/__init__.py
155
157
  ai_review/tests/suites/services/review/inline/__init__.py
156
158
  ai_review/tests/suites/services/review/inline/test_schema.py
@@ -1,19 +0,0 @@
1
- import re
2
-
3
- CONTROL_CHARS_RE = re.compile(r"[\x00-\x1F]")
4
-
5
-
6
- def sanitize_json_string(raw: str) -> str:
7
- def replace(match: re.Match) -> str:
8
- char = match.group()
9
- match char:
10
- case "\n":
11
- return "\\n"
12
- case "\r":
13
- return "\\r"
14
- case "\t":
15
- return "\\t"
16
- case _:
17
- return f"\\u{ord(char):04x}"
18
-
19
- return CONTROL_CHARS_RE.sub(replace, raw)
File without changes
File without changes
File without changes