xai-review 0.5.0__tar.gz → 0.7.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 (170) hide show
  1. {xai_review-0.5.0 → xai_review-0.7.0}/PKG-INFO +25 -6
  2. {xai_review-0.5.0 → xai_review-0.7.0}/README.md +24 -5
  3. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/prompt.py +1 -0
  4. xai_review-0.7.0/ai_review/libs/template/render.py +13 -0
  5. xai_review-0.7.0/ai_review/services/prompt/schema.py +52 -0
  6. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/prompt/service.py +6 -6
  7. xai_review-0.7.0/ai_review/tests/suites/clients/gitlab/test_client.py +35 -0
  8. xai_review-0.7.0/ai_review/tests/suites/libs/config/test_prompt.py +57 -0
  9. xai_review-0.7.0/ai_review/tests/suites/libs/template/test_render.py +64 -0
  10. xai_review-0.7.0/ai_review/tests/suites/services/prompt/test_schema.py +71 -0
  11. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/prompt/test_service.py +8 -0
  12. xai_review-0.7.0/ai_review/tests/suites/services/review/__init__.py +0 -0
  13. xai_review-0.7.0/ai_review/tests/suites/services/review/inline/__init__.py +0 -0
  14. xai_review-0.7.0/ai_review/tests/suites/services/review/policy/__init__.py +0 -0
  15. xai_review-0.7.0/ai_review/tests/suites/services/review/summary/__init__.py +0 -0
  16. {xai_review-0.5.0 → xai_review-0.7.0}/pyproject.toml +1 -1
  17. {xai_review-0.5.0 → xai_review-0.7.0}/xai_review.egg-info/PKG-INFO +25 -6
  18. {xai_review-0.5.0 → xai_review-0.7.0}/xai_review.egg-info/SOURCES.txt +8 -0
  19. xai_review-0.5.0/ai_review/services/prompt/schema.py +0 -71
  20. xai_review-0.5.0/ai_review/tests/suites/services/prompt/test_schema.py +0 -38
  21. {xai_review-0.5.0 → xai_review-0.7.0}/LICENSE +0 -0
  22. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/__init__.py +0 -0
  23. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/__init__.py +0 -0
  24. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/commands/__init__.py +0 -0
  25. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/commands/run_context_review.py +0 -0
  26. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/commands/run_inline_review.py +0 -0
  27. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/commands/run_review.py +0 -0
  28. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/commands/run_summary_review.py +0 -0
  29. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/cli/main.py +0 -0
  30. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/__init__.py +0 -0
  31. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/claude/__init__.py +0 -0
  32. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/claude/client.py +0 -0
  33. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/claude/schema.py +0 -0
  34. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gemini/__init__.py +0 -0
  35. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gemini/client.py +0 -0
  36. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gemini/schema.py +0 -0
  37. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/__init__.py +0 -0
  38. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/client.py +0 -0
  39. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/__init__.py +0 -0
  40. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/client.py +0 -0
  41. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/schema/__init__.py +0 -0
  42. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/schema/changes.py +0 -0
  43. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/schema/comments.py +0 -0
  44. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/gitlab/mr/schema/discussions.py +0 -0
  45. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/openai/__init__.py +0 -0
  46. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/openai/client.py +0 -0
  47. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/clients/openai/schema.py +0 -0
  48. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/config.py +0 -0
  49. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/__init__.py +0 -0
  50. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/asynchronous/__init__.py +0 -0
  51. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/asynchronous/gather.py +0 -0
  52. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/__init__.py +0 -0
  53. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/artifacts.py +0 -0
  54. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/base.py +0 -0
  55. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/claude.py +0 -0
  56. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/gemini.py +0 -0
  57. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/gitlab.py +0 -0
  58. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/http.py +0 -0
  59. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/llm.py +0 -0
  60. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/logger.py +0 -0
  61. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/openai.py +0 -0
  62. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/review.py +0 -0
  63. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/config/vcs.py +0 -0
  64. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/constants/__init__.py +0 -0
  65. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/constants/llm_provider.py +0 -0
  66. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/constants/vcs_provider.py +0 -0
  67. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/diff/__init__.py +0 -0
  68. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/diff/models.py +0 -0
  69. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/diff/parser.py +0 -0
  70. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/diff/tools.py +0 -0
  71. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/__init__.py +0 -0
  72. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/client.py +0 -0
  73. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/event_hooks/__init__.py +0 -0
  74. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/event_hooks/base.py +0 -0
  75. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/event_hooks/logger.py +0 -0
  76. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/handlers.py +0 -0
  77. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/transports/__init__.py +0 -0
  78. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/http/transports/retry.py +0 -0
  79. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/logger.py +0 -0
  80. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/libs/resources.py +0 -0
  81. {xai_review-0.5.0/ai_review/prompts → xai_review-0.7.0/ai_review/libs/template}/__init__.py +0 -0
  82. {xai_review-0.5.0/ai_review/resources → xai_review-0.7.0/ai_review/prompts}/__init__.py +0 -0
  83. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_context.md +0 -0
  84. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_inline.md +0 -0
  85. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_summary.md +0 -0
  86. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_system_context.md +0 -0
  87. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_system_inline.md +0 -0
  88. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/prompts/default_system_summary.md +0 -0
  89. {xai_review-0.5.0/ai_review/services → xai_review-0.7.0/ai_review/resources}/__init__.py +0 -0
  90. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/resources/pricing.yaml +0 -0
  91. {xai_review-0.5.0/ai_review/services/artifacts → xai_review-0.7.0/ai_review/services}/__init__.py +0 -0
  92. {xai_review-0.5.0/ai_review/services/cost → xai_review-0.7.0/ai_review/services/artifacts}/__init__.py +0 -0
  93. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/artifacts/schema.py +0 -0
  94. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/artifacts/service.py +0 -0
  95. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/artifacts/tools.py +0 -0
  96. {xai_review-0.5.0/ai_review/services/diff → xai_review-0.7.0/ai_review/services/cost}/__init__.py +0 -0
  97. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/cost/schema.py +0 -0
  98. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/cost/service.py +0 -0
  99. {xai_review-0.5.0/ai_review/services/git → xai_review-0.7.0/ai_review/services/diff}/__init__.py +0 -0
  100. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/diff/renderers.py +0 -0
  101. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/diff/schema.py +0 -0
  102. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/diff/service.py +0 -0
  103. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/diff/tools.py +0 -0
  104. {xai_review-0.5.0/ai_review/services/llm → xai_review-0.7.0/ai_review/services/git}/__init__.py +0 -0
  105. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/git/service.py +0 -0
  106. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/git/types.py +0 -0
  107. {xai_review-0.5.0/ai_review/services/llm/claude → xai_review-0.7.0/ai_review/services/llm}/__init__.py +0 -0
  108. {xai_review-0.5.0/ai_review/services/llm/gemini → xai_review-0.7.0/ai_review/services/llm/claude}/__init__.py +0 -0
  109. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/llm/claude/client.py +0 -0
  110. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/llm/factory.py +0 -0
  111. {xai_review-0.5.0/ai_review/services/llm/openai → xai_review-0.7.0/ai_review/services/llm/gemini}/__init__.py +0 -0
  112. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/llm/gemini/client.py +0 -0
  113. {xai_review-0.5.0/ai_review/services/prompt → xai_review-0.7.0/ai_review/services/llm/openai}/__init__.py +0 -0
  114. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/llm/openai/client.py +0 -0
  115. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/llm/types.py +0 -0
  116. {xai_review-0.5.0/ai_review/services/review → xai_review-0.7.0/ai_review/services/prompt}/__init__.py +0 -0
  117. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/prompt/adapter.py +0 -0
  118. {xai_review-0.5.0/ai_review/services/review/inline → xai_review-0.7.0/ai_review/services/review}/__init__.py +0 -0
  119. {xai_review-0.5.0/ai_review/services/review/policy → xai_review-0.7.0/ai_review/services/review/inline}/__init__.py +0 -0
  120. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/inline/schema.py +0 -0
  121. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/inline/service.py +0 -0
  122. {xai_review-0.5.0/ai_review/services/review/summary → xai_review-0.7.0/ai_review/services/review/policy}/__init__.py +0 -0
  123. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/policy/service.py +0 -0
  124. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/service.py +0 -0
  125. {xai_review-0.5.0/ai_review/services/vcs → xai_review-0.7.0/ai_review/services/review/summary}/__init__.py +0 -0
  126. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/summary/schema.py +0 -0
  127. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/review/summary/service.py +0 -0
  128. {xai_review-0.5.0/ai_review/services/vcs/gitlab → xai_review-0.7.0/ai_review/services/vcs}/__init__.py +0 -0
  129. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/vcs/factory.py +0 -0
  130. {xai_review-0.5.0/ai_review/tests → xai_review-0.7.0/ai_review/services/vcs/gitlab}/__init__.py +0 -0
  131. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/vcs/gitlab/client.py +0 -0
  132. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/services/vcs/types.py +0 -0
  133. {xai_review-0.5.0/ai_review/tests/fixtures → xai_review-0.7.0/ai_review/tests}/__init__.py +0 -0
  134. {xai_review-0.5.0/ai_review/tests/suites → xai_review-0.7.0/ai_review/tests/fixtures}/__init__.py +0 -0
  135. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/fixtures/git.py +0 -0
  136. {xai_review-0.5.0/ai_review/tests/suites/clients → xai_review-0.7.0/ai_review/tests/suites}/__init__.py +0 -0
  137. {xai_review-0.5.0/ai_review/tests/suites/clients/claude → xai_review-0.7.0/ai_review/tests/suites/clients}/__init__.py +0 -0
  138. {xai_review-0.5.0/ai_review/tests/suites/clients/gemini → xai_review-0.7.0/ai_review/tests/suites/clients/claude}/__init__.py +0 -0
  139. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/claude/test_client.py +0 -0
  140. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/claude/test_schema.py +0 -0
  141. {xai_review-0.5.0/ai_review/tests/suites/clients/openai → xai_review-0.7.0/ai_review/tests/suites/clients/gemini}/__init__.py +0 -0
  142. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/gemini/test_client.py +0 -0
  143. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/gemini/test_schema.py +0 -0
  144. {xai_review-0.5.0/ai_review/tests/suites/libs → xai_review-0.7.0/ai_review/tests/suites/clients/gitlab}/__init__.py +0 -0
  145. {xai_review-0.5.0/ai_review/tests/suites/libs/diff → xai_review-0.7.0/ai_review/tests/suites/clients/openai}/__init__.py +0 -0
  146. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/openai/test_client.py +0 -0
  147. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/clients/openai/test_schema.py +0 -0
  148. {xai_review-0.5.0/ai_review/tests/suites/services → xai_review-0.7.0/ai_review/tests/suites/libs}/__init__.py +0 -0
  149. {xai_review-0.5.0/ai_review/tests/suites/services/diff → xai_review-0.7.0/ai_review/tests/suites/libs/config}/__init__.py +0 -0
  150. {xai_review-0.5.0/ai_review/tests/suites/services/prompt → xai_review-0.7.0/ai_review/tests/suites/libs/diff}/__init__.py +0 -0
  151. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/libs/diff/test_models.py +0 -0
  152. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/libs/diff/test_parser.py +0 -0
  153. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/libs/diff/test_tools.py +0 -0
  154. {xai_review-0.5.0/ai_review/tests/suites/services/review → xai_review-0.7.0/ai_review/tests/suites/libs/template}/__init__.py +0 -0
  155. {xai_review-0.5.0/ai_review/tests/suites/services/review/inline → xai_review-0.7.0/ai_review/tests/suites/services}/__init__.py +0 -0
  156. {xai_review-0.5.0/ai_review/tests/suites/services/review/policy → xai_review-0.7.0/ai_review/tests/suites/services/diff}/__init__.py +0 -0
  157. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/diff/test_renderers.py +0 -0
  158. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/diff/test_service.py +0 -0
  159. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/diff/test_tools.py +0 -0
  160. {xai_review-0.5.0/ai_review/tests/suites/services/review/summary → xai_review-0.7.0/ai_review/tests/suites/services/prompt}/__init__.py +0 -0
  161. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/review/inline/test_schema.py +0 -0
  162. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/review/inline/test_service.py +0 -0
  163. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/review/policy/test_service.py +0 -0
  164. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/review/summary/test_schema.py +0 -0
  165. {xai_review-0.5.0 → xai_review-0.7.0}/ai_review/tests/suites/services/review/summary/test_service.py +0 -0
  166. {xai_review-0.5.0 → xai_review-0.7.0}/setup.cfg +0 -0
  167. {xai_review-0.5.0 → xai_review-0.7.0}/xai_review.egg-info/dependency_links.txt +0 -0
  168. {xai_review-0.5.0 → xai_review-0.7.0}/xai_review.egg-info/entry_points.txt +0 -0
  169. {xai_review-0.5.0 → xai_review-0.7.0}/xai_review.egg-info/requires.txt +0 -0
  170. {xai_review-0.5.0 → xai_review-0.7.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.5.0
3
+ Version: 0.7.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>
@@ -211,19 +211,38 @@ ai-review:
211
211
  # Custom context variables
212
212
  # ===============================
213
213
  # You can inject custom variables into prompts via PROMPT__CONTEXT__*.
214
- # These will be available as placeholders {var} in all templates.
214
+ # These will be available in all templates through placeholders.
215
+ #
216
+ # Placeholder syntax is defined separately in PROMPT__CONTEXT_PLACEHOLDER.
217
+ # Default: <<{value}>>
215
218
  #
216
219
  # Example usage in prompt templates:
217
- # Project: {company_name}
218
- # Env: {environment}
219
- # Pipeline: {ci_pipeline_url}
220
+ # Project: <<company_name>>
221
+ # Env: <<environment>>
222
+ # Pipeline: <<ci_pipeline_url>>
220
223
  #
221
224
  # Values override built-in variables if names collide.
222
- # To avoid clashes, prefer namespaced keys (ci_pipeline_url, org_notify_handle, env_name).
225
+ # To avoid clashes, prefer namespaced keys
226
+ # (ci_pipeline_url, org_notify_handle, env_name).
223
227
  #
224
228
  # PROMPT__CONTEXT__ENVIRONMENT: "staging"
225
229
  # PROMPT__CONTEXT__COMPANY_NAME: "ACME Corp"
226
230
  # PROMPT__CONTEXT__CI_PIPELINE_URL: "https://gitlab.com/pipelines/123"
231
+ #
232
+ # ===============================
233
+ # Context placeholder
234
+ # ===============================
235
+ # Defines how placeholders are written in prompt templates.
236
+ # Must contain "{value}" which will be replaced by the variable name.
237
+ #
238
+ # Default: <<{value}>>
239
+ #
240
+ # Example:
241
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
242
+ # Template: "Env: <<environment>>"
243
+ # Result: "Env: staging"
244
+ #
245
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
227
246
 
228
247
  # ===============================
229
248
  # Review options
@@ -187,19 +187,38 @@ ai-review:
187
187
  # Custom context variables
188
188
  # ===============================
189
189
  # You can inject custom variables into prompts via PROMPT__CONTEXT__*.
190
- # These will be available as placeholders {var} in all templates.
190
+ # These will be available in all templates through placeholders.
191
+ #
192
+ # Placeholder syntax is defined separately in PROMPT__CONTEXT_PLACEHOLDER.
193
+ # Default: <<{value}>>
191
194
  #
192
195
  # Example usage in prompt templates:
193
- # Project: {company_name}
194
- # Env: {environment}
195
- # Pipeline: {ci_pipeline_url}
196
+ # Project: <<company_name>>
197
+ # Env: <<environment>>
198
+ # Pipeline: <<ci_pipeline_url>>
196
199
  #
197
200
  # Values override built-in variables if names collide.
198
- # To avoid clashes, prefer namespaced keys (ci_pipeline_url, org_notify_handle, env_name).
201
+ # To avoid clashes, prefer namespaced keys
202
+ # (ci_pipeline_url, org_notify_handle, env_name).
199
203
  #
200
204
  # PROMPT__CONTEXT__ENVIRONMENT: "staging"
201
205
  # PROMPT__CONTEXT__COMPANY_NAME: "ACME Corp"
202
206
  # PROMPT__CONTEXT__CI_PIPELINE_URL: "https://gitlab.com/pipelines/123"
207
+ #
208
+ # ===============================
209
+ # Context placeholder
210
+ # ===============================
211
+ # Defines how placeholders are written in prompt templates.
212
+ # Must contain "{value}" which will be replaced by the variable name.
213
+ #
214
+ # Default: <<{value}>>
215
+ #
216
+ # Example:
217
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
218
+ # Template: "Env: <<environment>>"
219
+ # Result: "Env: staging"
220
+ #
221
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
203
222
 
204
223
  # ===============================
205
224
  # Review options
@@ -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
+ context_placeholder: str = "<<{value}>>"
11
12
  inline_prompt_files: list[FilePath] | None = None
12
13
  context_prompt_files: list[FilePath] | None = None
13
14
  summary_prompt_files: list[FilePath] | None = None
@@ -0,0 +1,13 @@
1
+ import re
2
+ from typing import Mapping
3
+
4
+
5
+ def render_template(text: str, values: Mapping[str, str], placeholder: str = "<<{value}>>") -> str:
6
+ regex_pattern = re.escape(placeholder).replace(r"\{value\}", r"([\w\.-]+)")
7
+ regex = re.compile(regex_pattern)
8
+
9
+ def replacer(match: re.Match) -> str:
10
+ key = match.group(1)
11
+ return values.get(key, match.group(0))
12
+
13
+ return regex.sub(replacer, text)
@@ -0,0 +1,52 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from ai_review.config import settings
4
+ from ai_review.libs.template.render import render_template
5
+
6
+
7
+ class PromptContextSchema(BaseModel):
8
+ merge_request_title: str = ""
9
+ merge_request_description: str = ""
10
+
11
+ merge_request_author_name: str = ""
12
+ merge_request_author_username: str = ""
13
+
14
+ merge_request_reviewer: str = ""
15
+ merge_request_reviewers: list[str] = Field(default_factory=list)
16
+ merge_request_reviewers_usernames: list[str] = Field(default_factory=list)
17
+
18
+ merge_request_assignees: list[str] = Field(default_factory=list)
19
+ merge_request_assignees_usernames: list[str] = Field(default_factory=list)
20
+
21
+ source_branch: str = ""
22
+ target_branch: str = ""
23
+
24
+ labels: list[str] = Field(default_factory=list)
25
+ changed_files: list[str] = Field(default_factory=list)
26
+
27
+ @property
28
+ def render_values(self) -> dict[str, str]:
29
+ return {
30
+ "merge_request_title": self.merge_request_title,
31
+ "merge_request_description": self.merge_request_description,
32
+
33
+ "merge_request_author_name": self.merge_request_author_name,
34
+ "merge_request_author_username": self.merge_request_author_username,
35
+
36
+ "merge_request_reviewer": self.merge_request_reviewer,
37
+ "merge_request_reviewers": ", ".join(self.merge_request_reviewers),
38
+ "merge_request_reviewers_usernames": ", ".join(self.merge_request_reviewers_usernames),
39
+
40
+ "merge_request_assignees": ", ".join(self.merge_request_assignees),
41
+ "merge_request_assignees_usernames": ", ".join(self.merge_request_assignees_usernames),
42
+
43
+ "source_branch": self.source_branch,
44
+ "target_branch": self.target_branch,
45
+
46
+ "labels": ", ".join(self.labels),
47
+ "changed_files": ", ".join(self.changed_files),
48
+ }
49
+
50
+ def apply_format(self, prompt: str) -> str:
51
+ values = {**self.render_values, **settings.prompt.context}
52
+ return render_template(prompt, values, settings.prompt.context_placeholder)
@@ -11,34 +11,34 @@ class PromptService:
11
11
  @classmethod
12
12
  def build_inline_request(cls, diff: DiffFileSchema, context: PromptContextSchema) -> str:
13
13
  inline_prompts = "\n\n".join(settings.prompt.load_inline())
14
- prompt = (
14
+ inline_prompts = context.apply_format(inline_prompts)
15
+ return (
15
16
  f"{inline_prompts}\n\n"
16
17
  f"## Diff\n\n"
17
18
  f"{format_file(diff)}"
18
19
  )
19
- return context.apply_format(prompt)
20
20
 
21
21
  @classmethod
22
22
  def build_summary_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
23
23
  changes = "\n\n".join(map(format_file, diffs))
24
24
  summary_prompts = "\n\n".join(settings.prompt.load_summary())
25
- prompt = (
25
+ summary_prompts = context.apply_format(summary_prompts)
26
+ return (
26
27
  f"{summary_prompts}\n\n"
27
28
  f"## Changes\n\n"
28
29
  f"{changes}\n"
29
30
  )
30
- return context.apply_format(prompt)
31
31
 
32
32
  @classmethod
33
33
  def build_context_request(cls, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
34
34
  changes = "\n\n".join(map(format_file, diffs))
35
35
  inline_prompts = "\n\n".join(settings.prompt.load_context())
36
- prompt = (
36
+ inline_prompts = context.apply_format(inline_prompts)
37
+ return (
37
38
  f"{inline_prompts}\n\n"
38
39
  f"## Diff\n\n"
39
40
  f"{changes}\n"
40
41
  )
41
- return context.apply_format(prompt)
42
42
 
43
43
  @classmethod
44
44
  def build_system_inline_request(cls, context: PromptContextSchema) -> str:
@@ -0,0 +1,35 @@
1
+ import pytest
2
+ from httpx import AsyncClient
3
+ from pydantic import HttpUrl, SecretStr
4
+
5
+ from ai_review.clients.gitlab.client import get_gitlab_http_client, GitLabHTTPClient
6
+ from ai_review.clients.gitlab.mr.client import GitLabMergeRequestsHTTPClient
7
+ from ai_review.config import settings
8
+ from ai_review.libs.config.gitlab import GitLabPipelineConfig, GitLabHTTPClientConfig
9
+ from ai_review.libs.config.vcs import GitLabVCSConfig
10
+ from ai_review.libs.constants.vcs_provider import VCSProvider
11
+
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def gitlab_http_client_config(monkeypatch: pytest.MonkeyPatch):
15
+ fake_config = GitLabVCSConfig(
16
+ provider=VCSProvider.GITLAB,
17
+ pipeline=GitLabPipelineConfig(
18
+ project_id="project-id",
19
+ merge_request_id="merge-request-id"
20
+ ),
21
+ http_client=GitLabHTTPClientConfig(
22
+ timeout=10,
23
+ api_url=HttpUrl("https://gitlab.com"),
24
+ api_token=SecretStr("fake-token"),
25
+ )
26
+ )
27
+ monkeypatch.setattr(settings, "vcs", fake_config)
28
+
29
+
30
+ def test_get_gitlab_http_client_builds_ok():
31
+ gitlab_http_client = get_gitlab_http_client()
32
+
33
+ assert isinstance(gitlab_http_client, GitLabHTTPClient)
34
+ assert isinstance(gitlab_http_client.mr, GitLabMergeRequestsHTTPClient)
35
+ assert isinstance(gitlab_http_client.mr.client, AsyncClient)
@@ -0,0 +1,57 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from ai_review.libs.config.prompt import PromptConfig
6
+
7
+
8
+ def test_inline_prompt_files_or_default_uses_defaults(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
9
+ dummy_file = tmp_path / "dummy.md"
10
+ dummy_file.write_text("DUMMY")
11
+ monkeypatch.setattr("ai_review.libs.config.prompt.load_resource", lambda **_: dummy_file)
12
+
13
+ config = PromptConfig()
14
+ result = config.inline_prompt_files_or_default
15
+
16
+ assert result == [dummy_file]
17
+ assert config.load_inline() == ["DUMMY"]
18
+
19
+
20
+ def test_system_inline_prompts_none_returns_global(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
21
+ dummy_file = tmp_path / "global.md"
22
+ dummy_file.write_text("GLOBAL")
23
+ monkeypatch.setattr("ai_review.libs.config.prompt.load_resource", lambda **_: dummy_file)
24
+
25
+ config = PromptConfig(system_inline_prompt_files=None)
26
+ result = config.system_inline_prompt_files_or_default
27
+
28
+ assert result == [dummy_file]
29
+ assert config.load_system_inline() == ["GLOBAL"]
30
+
31
+
32
+ def test_system_inline_prompts_include_true(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
33
+ global_file = tmp_path / "global.md"
34
+ global_file.write_text("GLOBAL")
35
+ custom_file = tmp_path / "custom.md"
36
+ custom_file.write_text("CUSTOM")
37
+ monkeypatch.setattr("ai_review.libs.config.prompt.load_resource", lambda **_: global_file)
38
+
39
+ config = PromptConfig(system_inline_prompt_files=[custom_file], include_inline_system_prompts=True)
40
+ result = config.system_inline_prompt_files_or_default
41
+
42
+ assert global_file in result and custom_file in result
43
+ assert config.load_system_inline() == ["GLOBAL", "CUSTOM"]
44
+
45
+
46
+ def test_system_inline_prompts_include_false(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
47
+ global_file = tmp_path / "global.md"
48
+ global_file.write_text("GLOBAL")
49
+ custom_file = tmp_path / "custom.md"
50
+ custom_file.write_text("CUSTOM")
51
+ monkeypatch.setattr("ai_review.libs.config.prompt.load_resource", lambda **_: global_file)
52
+
53
+ config = PromptConfig(system_inline_prompt_files=[custom_file], include_inline_system_prompts=False)
54
+ result = config.system_inline_prompt_files_or_default
55
+
56
+ assert result == [custom_file]
57
+ assert config.load_system_inline() == ["CUSTOM"]
@@ -0,0 +1,64 @@
1
+ from ai_review.libs.template.render import render_template
2
+
3
+
4
+ def test_replaces_single_variable():
5
+ text = "Hello, <<name>>!"
6
+ values = {"name": "Alice"}
7
+ result = render_template(text, values)
8
+ assert result == "Hello, Alice!"
9
+
10
+
11
+ def test_replaces_multiple_variables():
12
+ text = "User: <<user>>, Branch: <<branch>>"
13
+ values = {"user": "nikita", "branch": "main"}
14
+ result = render_template(text, values)
15
+ assert result == "User: nikita, Branch: main"
16
+
17
+
18
+ def test_missing_variable_keeps_placeholder():
19
+ text = "Hello, <<name>>, you are <<role>>"
20
+ values = {"name": "Bob"}
21
+ result = render_template(text, values)
22
+ assert result == "Hello, Bob, you are <<role>>"
23
+
24
+
25
+ def test_custom_placeholder_format():
26
+ text = "Hello, [[name]]!"
27
+ values = {"name": "Alice"}
28
+ result = render_template(text, values, placeholder="[[{value}]]")
29
+ assert result == "Hello, Alice!"
30
+
31
+
32
+ def test_placeholder_with_digits_and_underscores():
33
+ text = "Key: <<var_123>>"
34
+ values = {"var_123": "ok"}
35
+ result = render_template(text, values)
36
+ assert result == "Key: ok"
37
+
38
+
39
+ def test_no_placeholders_in_text():
40
+ text = "Nothing to replace"
41
+ values = {"name": "Alice"}
42
+ result = render_template(text, values)
43
+ assert result == "Nothing to replace"
44
+
45
+
46
+ def test_partial_overlap_does_not_replace():
47
+ text = "This is <<var>> and <<var_extra>>"
48
+ values = {"var": "A", "var_extra": "B"}
49
+ result = render_template(text, values)
50
+ assert result == "This is A and B"
51
+
52
+
53
+ def test_placeholder_with_dot_and_dash():
54
+ text = "Branch: <<feature-1.2>>"
55
+ values = {"feature-1.2": "ok"}
56
+ result = render_template(text, values)
57
+ assert result == "Branch: ok"
58
+
59
+
60
+ def test_multiple_same_placeholders():
61
+ text = "<<name>>, <<name>>, <<name>>!"
62
+ values = {"name": "Alice"}
63
+ result = render_template(text, values)
64
+ assert result == "Alice, Alice, Alice!"
@@ -0,0 +1,71 @@
1
+ import pytest
2
+
3
+ from ai_review.config import settings
4
+ from ai_review.services.prompt.schema import PromptContextSchema
5
+
6
+
7
+ def test_apply_format_inserts_variables() -> None:
8
+ context = PromptContextSchema(
9
+ merge_request_title="My MR",
10
+ merge_request_author_username="nikita"
11
+ )
12
+ template = "Title: <<merge_request_title>>, Author: @<<merge_request_author_username>>"
13
+ result = context.apply_format(template)
14
+ assert result == "Title: My MR, Author: @nikita"
15
+
16
+
17
+ def test_apply_format_with_lists() -> None:
18
+ context = PromptContextSchema(
19
+ merge_request_reviewers=["Alice", "Bob"],
20
+ merge_request_reviewers_usernames=["alice", "bob"],
21
+ labels=["bug", "feature"],
22
+ changed_files=["a.py", "b.py"],
23
+ )
24
+ template = (
25
+ "Reviewers: <<merge_request_reviewers>>\n"
26
+ "Usernames: <<merge_request_reviewers_usernames>>\n"
27
+ "Labels: <<labels>>\n"
28
+ "Files: <<changed_files>>"
29
+ )
30
+ result = context.apply_format(template)
31
+ assert "Alice, Bob" in result
32
+ assert "alice, bob" in result
33
+ assert "bug, feature" in result
34
+ assert "a.py, b.py" in result
35
+
36
+
37
+ def test_apply_format_handles_missing_fields() -> None:
38
+ context = PromptContextSchema()
39
+ template = "Title: <<merge_request_title>>, Reviewer: <<merge_request_reviewer>>"
40
+ result = context.apply_format(template)
41
+ assert result == "Title: , Reviewer: "
42
+
43
+
44
+ def test_apply_format_unknown_placeholder_kept() -> None:
45
+ context = PromptContextSchema()
46
+ template = "Unknown: <<does_not_exist>>"
47
+ result = context.apply_format(template)
48
+ assert result == "Unknown: <<does_not_exist>>"
49
+
50
+
51
+ def test_apply_format_multiple_occurrences() -> None:
52
+ context = PromptContextSchema(merge_request_title="My MR")
53
+ template = "<<merge_request_title>> again <<merge_request_title>>"
54
+ result = context.apply_format(template)
55
+ assert result == "My MR again My MR"
56
+
57
+
58
+ def test_apply_format_override_from_settings(monkeypatch: pytest.MonkeyPatch) -> None:
59
+ monkeypatch.setitem(settings.prompt.context, "merge_request_title", "Overridden")
60
+ context = PromptContextSchema(merge_request_title="Local Value")
61
+ template = "Title: <<merge_request_title>>"
62
+ result = context.apply_format(template)
63
+ assert result == "Title: Overridden"
64
+
65
+
66
+ def test_apply_format_prefers_override_even_if_empty(monkeypatch: pytest.MonkeyPatch) -> None:
67
+ monkeypatch.setitem(settings.prompt.context, "merge_request_title", "")
68
+ context = PromptContextSchema(merge_request_title="Local Value")
69
+ template = "Title: <<merge_request_title>>"
70
+ result = context.apply_format(template)
71
+ assert result == "Title: "
@@ -126,3 +126,11 @@ def test_build_system_summary_request_empty(
126
126
  monkeypatch.setattr(PromptConfig, "load_system_summary", lambda self: [])
127
127
  result = PromptService.build_system_summary_request(dummy_context)
128
128
  assert result == ""
129
+
130
+
131
+ def test_diff_placeholders_are_not_replaced(dummy_context: PromptContextSchema) -> None:
132
+ diffs = [DiffFileSchema(file="x.py", diff='print("<<merge_request_title>>")')]
133
+ result = PromptService.build_summary_request(diffs, dummy_context)
134
+
135
+ assert "<<merge_request_title>>" in result
136
+ assert "Fix login bug" not in result
@@ -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.5.0"
8
+ version = "0.7.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.5.0
3
+ Version: 0.7.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>
@@ -211,19 +211,38 @@ ai-review:
211
211
  # Custom context variables
212
212
  # ===============================
213
213
  # You can inject custom variables into prompts via PROMPT__CONTEXT__*.
214
- # These will be available as placeholders {var} in all templates.
214
+ # These will be available in all templates through placeholders.
215
+ #
216
+ # Placeholder syntax is defined separately in PROMPT__CONTEXT_PLACEHOLDER.
217
+ # Default: <<{value}>>
215
218
  #
216
219
  # Example usage in prompt templates:
217
- # Project: {company_name}
218
- # Env: {environment}
219
- # Pipeline: {ci_pipeline_url}
220
+ # Project: <<company_name>>
221
+ # Env: <<environment>>
222
+ # Pipeline: <<ci_pipeline_url>>
220
223
  #
221
224
  # Values override built-in variables if names collide.
222
- # To avoid clashes, prefer namespaced keys (ci_pipeline_url, org_notify_handle, env_name).
225
+ # To avoid clashes, prefer namespaced keys
226
+ # (ci_pipeline_url, org_notify_handle, env_name).
223
227
  #
224
228
  # PROMPT__CONTEXT__ENVIRONMENT: "staging"
225
229
  # PROMPT__CONTEXT__COMPANY_NAME: "ACME Corp"
226
230
  # PROMPT__CONTEXT__CI_PIPELINE_URL: "https://gitlab.com/pipelines/123"
231
+ #
232
+ # ===============================
233
+ # Context placeholder
234
+ # ===============================
235
+ # Defines how placeholders are written in prompt templates.
236
+ # Must contain "{value}" which will be replaced by the variable name.
237
+ #
238
+ # Default: <<{value}>>
239
+ #
240
+ # Example:
241
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
242
+ # Template: "Env: <<environment>>"
243
+ # Result: "Env: staging"
244
+ #
245
+ # PROMPT__CONTEXT_PLACEHOLDER: "<<{value}>>"
227
246
 
228
247
  # ===============================
229
248
  # Review options
@@ -61,6 +61,8 @@ ai_review/libs/http/event_hooks/base.py
61
61
  ai_review/libs/http/event_hooks/logger.py
62
62
  ai_review/libs/http/transports/__init__.py
63
63
  ai_review/libs/http/transports/retry.py
64
+ ai_review/libs/template/__init__.py
65
+ ai_review/libs/template/render.py
64
66
  ai_review/prompts/__init__.py
65
67
  ai_review/prompts/default_context.md
66
68
  ai_review/prompts/default_inline.md
@@ -125,14 +127,20 @@ ai_review/tests/suites/clients/claude/test_schema.py
125
127
  ai_review/tests/suites/clients/gemini/__init__.py
126
128
  ai_review/tests/suites/clients/gemini/test_client.py
127
129
  ai_review/tests/suites/clients/gemini/test_schema.py
130
+ ai_review/tests/suites/clients/gitlab/__init__.py
131
+ ai_review/tests/suites/clients/gitlab/test_client.py
128
132
  ai_review/tests/suites/clients/openai/__init__.py
129
133
  ai_review/tests/suites/clients/openai/test_client.py
130
134
  ai_review/tests/suites/clients/openai/test_schema.py
131
135
  ai_review/tests/suites/libs/__init__.py
136
+ ai_review/tests/suites/libs/config/__init__.py
137
+ ai_review/tests/suites/libs/config/test_prompt.py
132
138
  ai_review/tests/suites/libs/diff/__init__.py
133
139
  ai_review/tests/suites/libs/diff/test_models.py
134
140
  ai_review/tests/suites/libs/diff/test_parser.py
135
141
  ai_review/tests/suites/libs/diff/test_tools.py
142
+ ai_review/tests/suites/libs/template/__init__.py
143
+ ai_review/tests/suites/libs/template/test_render.py
136
144
  ai_review/tests/suites/services/__init__.py
137
145
  ai_review/tests/suites/services/diff/__init__.py
138
146
  ai_review/tests/suites/services/diff/test_renderers.py
@@ -1,71 +0,0 @@
1
- from pydantic import BaseModel, Field
2
-
3
- from ai_review.config import settings
4
-
5
-
6
- class PromptContextSchema(BaseModel):
7
- merge_request_title: str | None = None
8
- merge_request_description: str | None = None
9
-
10
- merge_request_author_name: str | None = None
11
- merge_request_author_username: str | None = None
12
-
13
- merge_request_reviewer: str | None = None
14
- merge_request_reviewers: list[str] = Field(default_factory=list)
15
- merge_request_reviewers_usernames: list[str] = Field(default_factory=list)
16
-
17
- merge_request_assignees: list[str] = Field(default_factory=list)
18
- merge_request_assignees_usernames: list[str] = Field(default_factory=list)
19
-
20
- source_branch: str | None = None
21
- target_branch: str | None = None
22
-
23
- labels: list[str] = Field(default_factory=list)
24
- changed_files: list[str] = Field(default_factory=list)
25
-
26
- @property
27
- def reviewers_format(self) -> str:
28
- return ", ".join(self.merge_request_reviewers)
29
-
30
- @property
31
- def reviewers_usernames_format(self) -> str:
32
- return ", ".join(self.merge_request_reviewers_usernames)
33
-
34
- @property
35
- def assignees_format(self) -> str:
36
- return ", ".join(self.merge_request_assignees)
37
-
38
- @property
39
- def assignees_usernames_format(self) -> str:
40
- return ", ".join(self.merge_request_assignees_usernames)
41
-
42
- @property
43
- def labels_format(self) -> str:
44
- return ", ".join(self.labels)
45
-
46
- @property
47
- def changed_files_format(self) -> str:
48
- return ", ".join(self.changed_files)
49
-
50
- def apply_format(self, prompt: str) -> str:
51
- return prompt.format(
52
- merge_request_title=self.merge_request_title or "",
53
- merge_request_description=self.merge_request_description or "",
54
-
55
- merge_request_author_name=self.merge_request_author_name or "",
56
- merge_request_author_username=self.merge_request_author_username or "",
57
-
58
- merge_request_reviewer=self.merge_request_reviewer or "",
59
- merge_request_reviewers=self.reviewers_format,
60
- merge_request_reviewers_usernames=self.reviewers_usernames_format,
61
-
62
- merge_request_assignees=self.assignees_format,
63
- merge_request_assignees_usernames=self.assignees_usernames_format,
64
-
65
- source_branch=self.source_branch or "",
66
- target_branch=self.target_branch or "",
67
-
68
- labels=self.labels_format,
69
- changed_files=self.changed_files_format,
70
- **settings.prompt.context
71
- )