xai-review 0.19.0__tar.gz → 0.21.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 (215) hide show
  1. {xai_review-0.19.0 → xai_review-0.21.0}/PKG-INFO +10 -7
  2. {xai_review-0.19.0 → xai_review-0.21.0}/README.md +8 -6
  3. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/artifacts/service.py +2 -1
  4. xai_review-0.21.0/ai_review/services/artifacts/types.py +20 -0
  5. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/cost/service.py +2 -1
  6. xai_review-0.21.0/ai_review/services/cost/types.py +12 -0
  7. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/diff/service.py +2 -1
  8. xai_review-0.21.0/ai_review/services/diff/types.py +28 -0
  9. xai_review-0.21.0/ai_review/services/hook/__init__.py +5 -0
  10. xai_review-0.21.0/ai_review/services/hook/constants.py +24 -0
  11. xai_review-0.21.0/ai_review/services/hook/service.py +162 -0
  12. xai_review-0.21.0/ai_review/services/hook/types.py +28 -0
  13. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/claude/client.py +2 -2
  14. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/factory.py +2 -2
  15. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/gemini/client.py +2 -2
  16. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/openai/client.py +2 -2
  17. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/types.py +1 -1
  18. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/prompt/service.py +2 -1
  19. xai_review-0.21.0/ai_review/services/prompt/types.py +27 -0
  20. xai_review-0.21.0/ai_review/services/review/gateway/comment.py +65 -0
  21. xai_review-0.21.0/ai_review/services/review/gateway/llm.py +40 -0
  22. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/inline/schema.py +2 -2
  23. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/inline/service.py +2 -1
  24. xai_review-0.21.0/ai_review/services/review/inline/types.py +11 -0
  25. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/service.py +23 -74
  26. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/summary/service.py +2 -1
  27. xai_review-0.21.0/ai_review/services/review/summary/types.py +8 -0
  28. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/vcs/factory.py +2 -2
  29. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/vcs/github/client.py +4 -2
  30. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/vcs/gitlab/client.py +4 -2
  31. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/vcs/types.py +1 -1
  32. xai_review-0.21.0/ai_review/tests/fixtures/artifacts.py +51 -0
  33. xai_review-0.21.0/ai_review/tests/fixtures/cost.py +48 -0
  34. xai_review-0.21.0/ai_review/tests/fixtures/diff.py +46 -0
  35. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/fixtures/git.py +11 -5
  36. xai_review-0.21.0/ai_review/tests/fixtures/llm.py +26 -0
  37. xai_review-0.21.0/ai_review/tests/fixtures/prompt.py +43 -0
  38. xai_review-0.21.0/ai_review/tests/fixtures/review/inline.py +25 -0
  39. xai_review-0.21.0/ai_review/tests/fixtures/review/summary.py +19 -0
  40. xai_review-0.21.0/ai_review/tests/fixtures/vcs.py +49 -0
  41. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/diff/test_service.py +3 -3
  42. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/diff/test_tools.py +9 -9
  43. xai_review-0.21.0/ai_review/tests/suites/services/hook/test_service.py +93 -0
  44. xai_review-0.21.0/ai_review/tests/suites/services/review/inline/__init__.py +0 -0
  45. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/review/inline/test_schema.py +10 -9
  46. xai_review-0.21.0/ai_review/tests/suites/services/review/policy/__init__.py +0 -0
  47. xai_review-0.21.0/ai_review/tests/suites/services/review/summary/__init__.py +0 -0
  48. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/review/summary/test_schema.py +0 -1
  49. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/review/summary/test_service.py +10 -7
  50. xai_review-0.21.0/ai_review/tests/suites/services/review/test_service.py +126 -0
  51. {xai_review-0.19.0 → xai_review-0.21.0}/pyproject.toml +2 -2
  52. {xai_review-0.19.0 → xai_review-0.21.0}/xai_review.egg-info/PKG-INFO +10 -7
  53. {xai_review-0.19.0 → xai_review-0.21.0}/xai_review.egg-info/SOURCES.txt +25 -0
  54. {xai_review-0.19.0 → xai_review-0.21.0}/xai_review.egg-info/requires.txt +1 -0
  55. {xai_review-0.19.0 → xai_review-0.21.0}/LICENSE +0 -0
  56. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/__init__.py +0 -0
  57. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/__init__.py +0 -0
  58. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/commands/__init__.py +0 -0
  59. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/commands/run_context_review.py +0 -0
  60. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/commands/run_inline_review.py +0 -0
  61. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/commands/run_review.py +0 -0
  62. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/commands/run_summary_review.py +0 -0
  63. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/cli/main.py +0 -0
  64. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/__init__.py +0 -0
  65. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/claude/__init__.py +0 -0
  66. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/claude/client.py +0 -0
  67. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/claude/schema.py +0 -0
  68. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gemini/__init__.py +0 -0
  69. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gemini/client.py +0 -0
  70. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gemini/schema.py +0 -0
  71. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/__init__.py +0 -0
  72. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/client.py +0 -0
  73. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/__init__.py +0 -0
  74. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/client.py +0 -0
  75. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/schema/__init__.py +0 -0
  76. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/schema/comments.py +0 -0
  77. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/schema/files.py +0 -0
  78. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/schema/pull_request.py +0 -0
  79. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/github/pr/schema/reviews.py +0 -0
  80. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/__init__.py +0 -0
  81. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/client.py +0 -0
  82. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/__init__.py +0 -0
  83. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/client.py +0 -0
  84. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/schema/__init__.py +0 -0
  85. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/schema/changes.py +0 -0
  86. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/schema/discussions.py +0 -0
  87. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/gitlab/mr/schema/notes.py +0 -0
  88. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/openai/__init__.py +0 -0
  89. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/openai/client.py +0 -0
  90. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/clients/openai/schema.py +0 -0
  91. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/config.py +0 -0
  92. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/__init__.py +0 -0
  93. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/asynchronous/__init__.py +0 -0
  94. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/asynchronous/gather.py +0 -0
  95. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/__init__.py +0 -0
  96. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/artifacts.py +0 -0
  97. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/base.py +0 -0
  98. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/claude.py +0 -0
  99. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/gemini.py +0 -0
  100. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/github.py +0 -0
  101. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/gitlab.py +0 -0
  102. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/http.py +0 -0
  103. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/llm.py +0 -0
  104. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/logger.py +0 -0
  105. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/openai.py +0 -0
  106. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/prompt.py +0 -0
  107. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/review.py +0 -0
  108. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/config/vcs.py +0 -0
  109. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/constants/__init__.py +0 -0
  110. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/constants/llm_provider.py +0 -0
  111. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/constants/vcs_provider.py +0 -0
  112. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/diff/__init__.py +0 -0
  113. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/diff/models.py +0 -0
  114. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/diff/parser.py +0 -0
  115. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/diff/tools.py +0 -0
  116. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/__init__.py +0 -0
  117. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/client.py +0 -0
  118. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/event_hooks/__init__.py +0 -0
  119. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/event_hooks/base.py +0 -0
  120. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/event_hooks/logger.py +0 -0
  121. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/handlers.py +0 -0
  122. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/transports/__init__.py +0 -0
  123. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/http/transports/retry.py +0 -0
  124. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/json.py +0 -0
  125. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/logger.py +0 -0
  126. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/resources.py +0 -0
  127. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/template/__init__.py +0 -0
  128. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/libs/template/render.py +0 -0
  129. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/__init__.py +0 -0
  130. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_context.md +0 -0
  131. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_inline.md +0 -0
  132. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_summary.md +0 -0
  133. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_system_context.md +0 -0
  134. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_system_inline.md +0 -0
  135. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/prompts/default_system_summary.md +0 -0
  136. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/resources/__init__.py +0 -0
  137. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/resources/pricing.yaml +0 -0
  138. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/__init__.py +0 -0
  139. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/artifacts/__init__.py +0 -0
  140. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/artifacts/schema.py +0 -0
  141. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/artifacts/tools.py +0 -0
  142. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/cost/__init__.py +0 -0
  143. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/cost/schema.py +0 -0
  144. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/diff/__init__.py +0 -0
  145. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/diff/renderers.py +0 -0
  146. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/diff/schema.py +0 -0
  147. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/diff/tools.py +0 -0
  148. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/git/__init__.py +0 -0
  149. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/git/service.py +0 -0
  150. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/git/types.py +0 -0
  151. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/__init__.py +0 -0
  152. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/claude/__init__.py +0 -0
  153. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/gemini/__init__.py +0 -0
  154. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/llm/openai/__init__.py +0 -0
  155. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/prompt/__init__.py +0 -0
  156. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/prompt/adapter.py +0 -0
  157. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/prompt/schema.py +0 -0
  158. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/prompt/tools.py +0 -0
  159. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/__init__.py +0 -0
  160. {xai_review-0.19.0/ai_review/services/review/inline → xai_review-0.21.0/ai_review/services/review/gateway}/__init__.py +0 -0
  161. {xai_review-0.19.0/ai_review/services/review/policy → xai_review-0.21.0/ai_review/services/review/inline}/__init__.py +0 -0
  162. {xai_review-0.19.0/ai_review/services/review/summary → xai_review-0.21.0/ai_review/services/review/policy}/__init__.py +0 -0
  163. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/policy/service.py +0 -0
  164. {xai_review-0.19.0/ai_review/services/vcs → xai_review-0.21.0/ai_review/services/review/summary}/__init__.py +0 -0
  165. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/services/review/summary/schema.py +0 -0
  166. {xai_review-0.19.0/ai_review/services/vcs/github → xai_review-0.21.0/ai_review/services/vcs}/__init__.py +0 -0
  167. {xai_review-0.19.0/ai_review/services/vcs/gitlab → xai_review-0.21.0/ai_review/services/vcs/github}/__init__.py +0 -0
  168. {xai_review-0.19.0/ai_review/tests → xai_review-0.21.0/ai_review/services/vcs/gitlab}/__init__.py +0 -0
  169. {xai_review-0.19.0/ai_review/tests/fixtures → xai_review-0.21.0/ai_review/tests}/__init__.py +0 -0
  170. {xai_review-0.19.0/ai_review/tests/suites → xai_review-0.21.0/ai_review/tests/fixtures}/__init__.py +0 -0
  171. {xai_review-0.19.0/ai_review/tests/suites/clients → xai_review-0.21.0/ai_review/tests/fixtures/review}/__init__.py +0 -0
  172. {xai_review-0.19.0/ai_review/tests/suites/clients/claude → xai_review-0.21.0/ai_review/tests/suites}/__init__.py +0 -0
  173. {xai_review-0.19.0/ai_review/tests/suites/clients/gemini → xai_review-0.21.0/ai_review/tests/suites/clients}/__init__.py +0 -0
  174. {xai_review-0.19.0/ai_review/tests/suites/clients/github → xai_review-0.21.0/ai_review/tests/suites/clients/claude}/__init__.py +0 -0
  175. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/claude/test_client.py +0 -0
  176. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/claude/test_schema.py +0 -0
  177. {xai_review-0.19.0/ai_review/tests/suites/clients/gitlab → xai_review-0.21.0/ai_review/tests/suites/clients/gemini}/__init__.py +0 -0
  178. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/gemini/test_client.py +0 -0
  179. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/gemini/test_schema.py +0 -0
  180. {xai_review-0.19.0/ai_review/tests/suites/clients/openai → xai_review-0.21.0/ai_review/tests/suites/clients/github}/__init__.py +0 -0
  181. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/github/test_client.py +0 -0
  182. {xai_review-0.19.0/ai_review/tests/suites/libs → xai_review-0.21.0/ai_review/tests/suites/clients/gitlab}/__init__.py +0 -0
  183. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/gitlab/test_client.py +0 -0
  184. {xai_review-0.19.0/ai_review/tests/suites/libs/config → xai_review-0.21.0/ai_review/tests/suites/clients/openai}/__init__.py +0 -0
  185. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/openai/test_client.py +0 -0
  186. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/clients/openai/test_schema.py +0 -0
  187. {xai_review-0.19.0/ai_review/tests/suites/libs/diff → xai_review-0.21.0/ai_review/tests/suites/libs}/__init__.py +0 -0
  188. {xai_review-0.19.0/ai_review/tests/suites/libs/template → xai_review-0.21.0/ai_review/tests/suites/libs/config}/__init__.py +0 -0
  189. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/config/test_prompt.py +0 -0
  190. {xai_review-0.19.0/ai_review/tests/suites/services → xai_review-0.21.0/ai_review/tests/suites/libs/diff}/__init__.py +0 -0
  191. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/diff/test_models.py +0 -0
  192. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/diff/test_parser.py +0 -0
  193. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/diff/test_tools.py +0 -0
  194. {xai_review-0.19.0/ai_review/tests/suites/services/cost → xai_review-0.21.0/ai_review/tests/suites/libs/template}/__init__.py +0 -0
  195. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/template/test_render.py +0 -0
  196. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/libs/test_json.py +0 -0
  197. {xai_review-0.19.0/ai_review/tests/suites/services/diff → xai_review-0.21.0/ai_review/tests/suites/services}/__init__.py +0 -0
  198. {xai_review-0.19.0/ai_review/tests/suites/services/prompt → xai_review-0.21.0/ai_review/tests/suites/services/cost}/__init__.py +0 -0
  199. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/cost/test_schema.py +0 -0
  200. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/cost/test_service.py +0 -0
  201. {xai_review-0.19.0/ai_review/tests/suites/services/review → xai_review-0.21.0/ai_review/tests/suites/services/diff}/__init__.py +0 -0
  202. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/diff/test_renderers.py +0 -0
  203. {xai_review-0.19.0/ai_review/tests/suites/services/review/inline → xai_review-0.21.0/ai_review/tests/suites/services/hook}/__init__.py +0 -0
  204. {xai_review-0.19.0/ai_review/tests/suites/services/review/policy → xai_review-0.21.0/ai_review/tests/suites/services/prompt}/__init__.py +0 -0
  205. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/prompt/test_adapter.py +0 -0
  206. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/prompt/test_schema.py +0 -0
  207. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/prompt/test_service.py +0 -0
  208. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/prompt/test_tools.py +0 -0
  209. {xai_review-0.19.0/ai_review/tests/suites/services/review/summary → xai_review-0.21.0/ai_review/tests/suites/services/review}/__init__.py +0 -0
  210. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/review/inline/test_service.py +0 -0
  211. {xai_review-0.19.0 → xai_review-0.21.0}/ai_review/tests/suites/services/review/policy/test_service.py +0 -0
  212. {xai_review-0.19.0 → xai_review-0.21.0}/setup.cfg +0 -0
  213. {xai_review-0.19.0 → xai_review-0.21.0}/xai_review.egg-info/dependency_links.txt +0 -0
  214. {xai_review-0.19.0 → xai_review-0.21.0}/xai_review.egg-info/entry_points.txt +0 -0
  215. {xai_review-0.19.0 → xai_review-0.21.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.19.0
3
+ Version: 0.21.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>
@@ -29,6 +29,7 @@ Requires-Dist: pydantic
29
29
  Requires-Dist: pydantic-settings
30
30
  Provides-Extra: test
31
31
  Requires-Dist: pytest; extra == "test"
32
+ Requires-Dist: pytest-asyncio; extra == "test"
32
33
  Dynamic: license-file
33
34
 
34
35
  # AI Review
@@ -82,11 +83,11 @@ final decision to human reviewers.
82
83
  Curious how **AI Review** works in practice? Here are three real Pull Requests reviewed entirely by the tool — one per
83
84
  mode:
84
85
 
85
- | Mode | Description | Live Example |
86
- |------------|----------------------------------------------------|-------------------------------------------------------------------------------|
87
- | 🧩 Inline | Adds line-by-line comments directly in the diff | [Try AI Review (inline)](https://github.com/Nikita-Filonov/ai-review/pull/4) |
88
- | 🧠 Context | Performs broader analysis across multiple files | [Try AI Review (context)](https://github.com/Nikita-Filonov/ai-review/pull/5) |
89
- | 📄 Summary | Posts a concise summary review with key highlights | [Try AI Review (summary)](https://github.com/Nikita-Filonov/ai-review/pull/6) |
86
+ | Mode | Description | 🐙 GitHub | 🦊 GitLab |
87
+ |------------|----------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------|
88
+ | 🧩 Inline | Adds line-by-line comments directly in the diff | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/4) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/2) |
89
+ | 🧠 Context | Performs broader analysis across multiple files | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/5) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/3) |
90
+ | 📄 Summary | Posts a concise summary review with key highlights | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/6) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/4) |
90
91
 
91
92
  👉 Each review was generated automatically via GitHub Actions using the corresponding mode:
92
93
 
@@ -192,6 +193,7 @@ Add a workflow like this (manual trigger from **Actions** tab):
192
193
 
193
194
  ```yaml
194
195
  name: AI Review
196
+
195
197
  on:
196
198
  workflow_dispatch:
197
199
  inputs:
@@ -207,7 +209,7 @@ jobs:
207
209
  runs-on: ubuntu-latest
208
210
  steps:
209
211
  - uses: actions/checkout@v4
210
- - uses: Nikita-Filonov/ai-review@v0.19.0
212
+ - uses: Nikita-Filonov/ai-review@v0.21.0
211
213
  with:
212
214
  review-command: ${{ inputs.review-command }}
213
215
  env:
@@ -272,6 +274,7 @@ ai-review:
272
274
  See these folders for reference templates and full configuration options:
273
275
 
274
276
  - [./docs/ci](./docs/ci) — CI/CD integration templates (GitHub Actions, GitLab CI)
277
+ - [./docs/hooks](./docs/hooks) — hook reference and lifecycle events
275
278
  - [./docs/configs](./docs/configs) — full configuration examples (`.yaml`, `.json`, `.env`)
276
279
  - [./docs/prompts](./docs/prompts) — prompt templates for Python/Go (light & strict modes)
277
280
 
@@ -49,11 +49,11 @@ final decision to human reviewers.
49
49
  Curious how **AI Review** works in practice? Here are three real Pull Requests reviewed entirely by the tool — one per
50
50
  mode:
51
51
 
52
- | Mode | Description | Live Example |
53
- |------------|----------------------------------------------------|-------------------------------------------------------------------------------|
54
- | 🧩 Inline | Adds line-by-line comments directly in the diff | [Try AI Review (inline)](https://github.com/Nikita-Filonov/ai-review/pull/4) |
55
- | 🧠 Context | Performs broader analysis across multiple files | [Try AI Review (context)](https://github.com/Nikita-Filonov/ai-review/pull/5) |
56
- | 📄 Summary | Posts a concise summary review with key highlights | [Try AI Review (summary)](https://github.com/Nikita-Filonov/ai-review/pull/6) |
52
+ | Mode | Description | 🐙 GitHub | 🦊 GitLab |
53
+ |------------|----------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------------------|
54
+ | 🧩 Inline | Adds line-by-line comments directly in the diff | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/4) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/2) |
55
+ | 🧠 Context | Performs broader analysis across multiple files | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/5) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/3) |
56
+ | 📄 Summary | Posts a concise summary review with key highlights | [View on GitHub](https://github.com/Nikita-Filonov/ai-review/pull/6) | [View on GitLab](https://gitlab.com/core8332439/review/-/merge_requests/4) |
57
57
 
58
58
  👉 Each review was generated automatically via GitHub Actions using the corresponding mode:
59
59
 
@@ -159,6 +159,7 @@ Add a workflow like this (manual trigger from **Actions** tab):
159
159
 
160
160
  ```yaml
161
161
  name: AI Review
162
+
162
163
  on:
163
164
  workflow_dispatch:
164
165
  inputs:
@@ -174,7 +175,7 @@ jobs:
174
175
  runs-on: ubuntu-latest
175
176
  steps:
176
177
  - uses: actions/checkout@v4
177
- - uses: Nikita-Filonov/ai-review@v0.19.0
178
+ - uses: Nikita-Filonov/ai-review@v0.21.0
178
179
  with:
179
180
  review-command: ${{ inputs.review-command }}
180
181
  env:
@@ -239,6 +240,7 @@ ai-review:
239
240
  See these folders for reference templates and full configuration options:
240
241
 
241
242
  - [./docs/ci](./docs/ci) — CI/CD integration templates (GitHub Actions, GitLab CI)
243
+ - [./docs/hooks](./docs/hooks) — hook reference and lifecycle events
242
244
  - [./docs/configs](./docs/configs) — full configuration examples (`.yaml`, `.json`, `.env`)
243
245
  - [./docs/prompts](./docs/prompts) — prompt templates for Python/Go (light & strict modes)
244
246
 
@@ -6,11 +6,12 @@ from ai_review.config import settings
6
6
  from ai_review.libs.logger import get_logger
7
7
  from ai_review.services.artifacts.schema import LLMArtifactSchema
8
8
  from ai_review.services.artifacts.tools import make_artifact_id
9
+ from ai_review.services.artifacts.types import ArtifactsServiceProtocol
9
10
 
10
11
  logger = get_logger("ARTIFACTS_SERVICE")
11
12
 
12
13
 
13
- class ArtifactsService:
14
+ class ArtifactsService(ArtifactsServiceProtocol):
14
15
  @classmethod
15
16
  async def save_llm_interaction(cls, prompt: str, prompt_system: str, response: str | None = None) -> str | None:
16
17
  if not settings.artifacts.llm_enabled:
@@ -0,0 +1,20 @@
1
+ from pathlib import Path
2
+ from typing import Protocol
3
+
4
+
5
+ class ArtifactsServiceProtocol(Protocol):
6
+ async def save_llm_interaction(
7
+ self,
8
+ prompt: str,
9
+ prompt_system: str,
10
+ response: str | None = None
11
+ ) -> str | None:
12
+ ...
13
+
14
+ async def save_artifact(
15
+ self,
16
+ file: Path,
17
+ content: str,
18
+ kind: str = "artifact"
19
+ ) -> Path | None:
20
+ ...
@@ -1,12 +1,13 @@
1
1
  from ai_review.config import settings
2
2
  from ai_review.libs.logger import get_logger
3
3
  from ai_review.services.cost.schema import CostReportSchema
4
+ from ai_review.services.cost.types import CostServiceProtocol
4
5
  from ai_review.services.llm.types import ChatResultSchema
5
6
 
6
7
  logger = get_logger("COST_SERVICE")
7
8
 
8
9
 
9
- class CostService:
10
+ class CostService(CostServiceProtocol):
10
11
  def __init__(self):
11
12
  self.pricing = settings.llm.load_pricing()
12
13
  self.reports: list[CostReportSchema] = []
@@ -0,0 +1,12 @@
1
+ from typing import Protocol
2
+
3
+ from ai_review.services.cost.schema import CostReportSchema
4
+ from ai_review.services.llm.types import ChatResultSchema
5
+
6
+
7
+ class CostServiceProtocol(Protocol):
8
+ def calculate(self, result: ChatResultSchema) -> CostReportSchema | None:
9
+ ...
10
+
11
+ def aggregate(self) -> CostReportSchema | None:
12
+ ...
@@ -16,12 +16,13 @@ from ai_review.services.diff.renderers import (
16
16
  )
17
17
  from ai_review.services.diff.schema import DiffFileSchema
18
18
  from ai_review.services.diff.tools import find_diff_file
19
+ from ai_review.services.diff.types import DiffServiceProtocol
19
20
  from ai_review.services.git.types import GitServiceProtocol
20
21
 
21
22
  logger = get_logger("DIFF_SERVICE")
22
23
 
23
24
 
24
- class DiffService:
25
+ class DiffService(DiffServiceProtocol):
25
26
  @classmethod
26
27
  def parse(cls, raw_diff: str) -> Diff:
27
28
  if not raw_diff.strip():
@@ -0,0 +1,28 @@
1
+ from typing import Protocol
2
+
3
+ from ai_review.libs.diff.models import Diff
4
+ from ai_review.services.diff.schema import DiffFileSchema
5
+ from ai_review.services.git.types import GitServiceProtocol
6
+
7
+
8
+ class DiffServiceProtocol(Protocol):
9
+ def parse(self, raw_diff: str) -> Diff:
10
+ ...
11
+
12
+ def render_file(
13
+ self,
14
+ file: str,
15
+ raw_diff: str,
16
+ base_sha: str | None = None,
17
+ head_sha: str | None = None,
18
+ ) -> DiffFileSchema:
19
+ ...
20
+
21
+ def render_files(
22
+ self,
23
+ git: GitServiceProtocol,
24
+ files: list[str],
25
+ base_sha: str,
26
+ head_sha: str,
27
+ ) -> list[DiffFileSchema]:
28
+ ...
@@ -0,0 +1,5 @@
1
+ from ai_review.services.hook.service import HookService
2
+
3
+ hook = HookService()
4
+
5
+ __all__ = ["hook"]
@@ -0,0 +1,24 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class HookType(StrEnum):
5
+ ON_CHAT_START = "ON_CHAT_START"
6
+ ON_CHAT_ERROR = "ON_CHAT_ERROR"
7
+ ON_CHAT_COMPLETE = "ON_CHAT_COMPLETE"
8
+
9
+ ON_INLINE_REVIEW_START = "ON_INLINE_REVIEW_START"
10
+ ON_INLINE_REVIEW_COMPLETE = "ON_INLINE_REVIEW_COMPLETE"
11
+
12
+ ON_CONTEXT_REVIEW_START = "ON_CONTEXT_REVIEW_START"
13
+ ON_CONTEXT_REVIEW_COMPLETE = "ON_CONTEXT_REVIEW_COMPLETE"
14
+
15
+ ON_SUMMARY_REVIEW_START = "ON_SUMMARY_REVIEW_START"
16
+ ON_SUMMARY_REVIEW_COMPLETE = "ON_SUMMARY_REVIEW_COMPLETE"
17
+
18
+ ON_INLINE_COMMENT_START = "ON_INLINE_COMMENT_START"
19
+ ON_INLINE_COMMENT_ERROR = "ON_INLINE_COMMENT_ERROR"
20
+ ON_INLINE_COMMENT_COMPLETE = "ON_INLINE_COMMENT_COMPLETE"
21
+
22
+ ON_SUMMARY_COMMENT_START = "ON_SUMMARY_COMMENT_START"
23
+ ON_SUMMARY_COMMENT_ERROR = "ON_SUMMARY_COMMENT_ERROR"
24
+ ON_SUMMARY_COMMENT_COMPLETE = "ON_SUMMARY_COMMENT_COMPLETE"
@@ -0,0 +1,162 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
4
+ from ai_review.libs.logger import get_logger
5
+ from ai_review.services.cost.schema import CostReportSchema
6
+ from ai_review.services.hook.constants import HookType
7
+ from ai_review.services.hook.types import (
8
+ HookFunc,
9
+ # --- Chat ---
10
+ ChatStartHookFunc,
11
+ ChatErrorHookFunc,
12
+ ChatCompleteHookFunc,
13
+ # --- Inline Review ---
14
+ InlineReviewStartHookFunc,
15
+ InlineReviewCompleteHookFunc,
16
+ # --- Context Review ---
17
+ ContextReviewStartHookFunc,
18
+ ContextReviewCompleteHookFunc,
19
+ # --- Summary Review ---
20
+ SummaryReviewStartHookFunc,
21
+ SummaryReviewCompleteHookFunc,
22
+ # --- Inline Comment ---
23
+ InlineCommentStartHookFunc,
24
+ InlineCommentErrorHookFunc,
25
+ InlineCommentCompleteHookFunc,
26
+ # --- Summary Comment ---
27
+ SummaryCommentStartHookFunc,
28
+ SummaryCommentCompleteHookFunc
29
+ )
30
+ from ai_review.services.review.inline.schema import InlineCommentSchema
31
+ from ai_review.services.review.summary.schema import SummaryCommentSchema
32
+
33
+ logger = get_logger("HOOK_SERVICE")
34
+
35
+
36
+ class HookService:
37
+ def __init__(self):
38
+ self.hooks: dict[HookType, list[HookFunc]] = defaultdict(list)
39
+
40
+ def inject_hook(self, name: HookType, func: HookFunc):
41
+ self.hooks[name].append(func)
42
+
43
+ async def emit(self, name: HookType, *args: Any, **kwargs: Any):
44
+ if not self.hooks.get(name):
45
+ return
46
+
47
+ for callback in self.hooks[name]:
48
+ try:
49
+ await callback(*args, **kwargs)
50
+ except Exception as error:
51
+ logger.exception(f"Error in {name} hook: {error}")
52
+
53
+ # --- Chat ---
54
+ def on_chat_start(self, func: ChatStartHookFunc):
55
+ self.inject_hook(HookType.ON_CHAT_START, func)
56
+ return func
57
+
58
+ def on_chat_error(self, func: ChatErrorHookFunc):
59
+ self.inject_hook(HookType.ON_CHAT_ERROR, func)
60
+ return func
61
+
62
+ def on_chat_complete(self, func: ChatCompleteHookFunc):
63
+ self.inject_hook(HookType.ON_CHAT_COMPLETE, func)
64
+ return func
65
+
66
+ async def emit_chat_start(self, prompt: str, prompt_system: str):
67
+ await self.emit(HookType.ON_CHAT_START, prompt=prompt, prompt_system=prompt_system)
68
+
69
+ async def emit_chat_error(self, prompt: str, prompt_system: str):
70
+ await self.emit(HookType.ON_CHAT_ERROR, prompt=prompt, prompt_system=prompt_system)
71
+
72
+ async def emit_chat_complete(self, result: str, report: CostReportSchema | None):
73
+ await self.emit(HookType.ON_CHAT_COMPLETE, result=result, report=report)
74
+
75
+ # --- Inline Review ---
76
+ def on_inline_review_start(self, func: InlineReviewStartHookFunc):
77
+ self.inject_hook(HookType.ON_INLINE_REVIEW_START, func)
78
+ return func
79
+
80
+ def on_inline_review_complete(self, func: InlineReviewCompleteHookFunc):
81
+ self.inject_hook(HookType.ON_INLINE_REVIEW_COMPLETE, func)
82
+ return func
83
+
84
+ async def emit_inline_review_start(self):
85
+ await self.emit(HookType.ON_INLINE_REVIEW_START)
86
+
87
+ async def emit_inline_review_complete(self, report: CostReportSchema | None):
88
+ await self.emit(HookType.ON_INLINE_REVIEW_COMPLETE, report=report)
89
+
90
+ # --- Context Review ---
91
+ def on_context_review_start(self, func: ContextReviewStartHookFunc):
92
+ self.inject_hook(HookType.ON_CONTEXT_REVIEW_START, func)
93
+ return func
94
+
95
+ def on_context_review_complete(self, func: ContextReviewCompleteHookFunc):
96
+ self.inject_hook(HookType.ON_CONTEXT_REVIEW_COMPLETE, func)
97
+ return func
98
+
99
+ async def emit_context_review_start(self):
100
+ await self.emit(HookType.ON_CONTEXT_REVIEW_START)
101
+
102
+ async def emit_context_review_complete(self, report: CostReportSchema | None):
103
+ await self.emit(HookType.ON_CONTEXT_REVIEW_COMPLETE, report=report)
104
+
105
+ # --- Summary Review ---
106
+ def on_summary_review_start(self, func: SummaryReviewStartHookFunc):
107
+ self.inject_hook(HookType.ON_SUMMARY_REVIEW_START, func)
108
+ return func
109
+
110
+ def on_summary_review_complete(self, func: SummaryReviewCompleteHookFunc):
111
+ self.inject_hook(HookType.ON_SUMMARY_REVIEW_COMPLETE, func)
112
+ return func
113
+
114
+ async def emit_summary_review_start(self):
115
+ await self.emit(HookType.ON_SUMMARY_REVIEW_START)
116
+
117
+ async def emit_summary_review_complete(self, report: CostReportSchema | None):
118
+ await self.emit(HookType.ON_SUMMARY_REVIEW_COMPLETE, report=report)
119
+
120
+ # --- Inline Comment ---
121
+ def on_inline_comment_start(self, func: InlineCommentStartHookFunc):
122
+ self.inject_hook(HookType.ON_INLINE_COMMENT_START, func)
123
+ return func
124
+
125
+ def on_inline_comment_error(self, func: InlineCommentErrorHookFunc):
126
+ self.inject_hook(HookType.ON_INLINE_COMMENT_ERROR, func)
127
+ return func
128
+
129
+ def on_inline_comment_complete(self, func: InlineCommentCompleteHookFunc):
130
+ self.inject_hook(HookType.ON_INLINE_COMMENT_COMPLETE, func)
131
+ return func
132
+
133
+ async def emit_inline_comment_start(self, comment: InlineCommentSchema):
134
+ await self.emit(HookType.ON_INLINE_COMMENT_START, comment=comment)
135
+
136
+ async def emit_inline_comment_error(self, comment: InlineCommentSchema):
137
+ await self.emit(HookType.ON_INLINE_COMMENT_ERROR, comment=comment)
138
+
139
+ async def emit_inline_comment_complete(self, comment: InlineCommentSchema):
140
+ await self.emit(HookType.ON_INLINE_COMMENT_COMPLETE, comment=comment)
141
+
142
+ # --- Summary Comment ---
143
+ def on_summary_comment_start(self, func: SummaryCommentStartHookFunc):
144
+ self.inject_hook(HookType.ON_SUMMARY_COMMENT_START, func)
145
+ return func
146
+
147
+ def on_summary_comment_error(self, func: InlineCommentErrorHookFunc):
148
+ self.inject_hook(HookType.ON_SUMMARY_COMMENT_ERROR, func)
149
+ return func
150
+
151
+ def on_summary_comment_complete(self, func: SummaryCommentCompleteHookFunc):
152
+ self.inject_hook(HookType.ON_SUMMARY_COMMENT_COMPLETE, func)
153
+ return func
154
+
155
+ async def emit_summary_comment_start(self, comment: SummaryCommentSchema):
156
+ await self.emit(HookType.ON_SUMMARY_COMMENT_START, comment=comment)
157
+
158
+ async def emit_summary_comment_error(self, comment: SummaryCommentSchema):
159
+ await self.emit(HookType.ON_SUMMARY_COMMENT_ERROR, comment=comment)
160
+
161
+ async def emit_summary_comment_complete(self, comment: SummaryCommentSchema):
162
+ await self.emit(HookType.ON_SUMMARY_COMMENT_COMPLETE, comment=comment)
@@ -0,0 +1,28 @@
1
+ from typing import Callable, Awaitable
2
+
3
+ from ai_review.services.cost.schema import CostReportSchema
4
+ from ai_review.services.review.inline.schema import InlineCommentSchema
5
+ from ai_review.services.review.summary.schema import SummaryCommentSchema
6
+
7
+ HookFunc = Callable[..., Awaitable[None]]
8
+
9
+ ChatStartHookFunc = Callable[[str, str], Awaitable[None]]
10
+ ChatErrorHookFunc = Callable[[str, str], Awaitable[None]]
11
+ ChatCompleteHookFunc = Callable[[str, CostReportSchema | None], Awaitable[None]]
12
+
13
+ InlineReviewStartHookFunc = Callable[..., Awaitable[None]]
14
+ InlineReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
15
+
16
+ ContextReviewStartHookFunc = Callable[..., Awaitable[None]]
17
+ ContextReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
18
+
19
+ SummaryReviewStartHookFunc = Callable[..., Awaitable[None]]
20
+ SummaryReviewCompleteHookFunc = Callable[[CostReportSchema | None], Awaitable[None]]
21
+
22
+ InlineCommentStartHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
23
+ InlineCommentErrorHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
24
+ InlineCommentCompleteHookFunc = Callable[[InlineCommentSchema], Awaitable[None]]
25
+
26
+ SummaryCommentStartHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
27
+ SummaryCommentErrorHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
28
+ SummaryCommentCompleteHookFunc = Callable[[SummaryCommentSchema], Awaitable[None]]
@@ -1,10 +1,10 @@
1
1
  from ai_review.clients.claude.client import get_claude_http_client
2
2
  from ai_review.clients.claude.schema import ClaudeChatRequestSchema, ClaudeMessageSchema
3
3
  from ai_review.config import settings
4
- from ai_review.services.llm.types import LLMClient, ChatResultSchema
4
+ from ai_review.services.llm.types import LLMClientProtocol, ChatResultSchema
5
5
 
6
6
 
7
- class ClaudeLLMClient(LLMClient):
7
+ class ClaudeLLMClient(LLMClientProtocol):
8
8
  def __init__(self):
9
9
  self.http_client = get_claude_http_client()
10
10
 
@@ -3,10 +3,10 @@ from ai_review.libs.constants.llm_provider import LLMProvider
3
3
  from ai_review.services.llm.claude.client import ClaudeLLMClient
4
4
  from ai_review.services.llm.gemini.client import GeminiLLMClient
5
5
  from ai_review.services.llm.openai.client import OpenAILLMClient
6
- from ai_review.services.llm.types import LLMClient
6
+ from ai_review.services.llm.types import LLMClientProtocol
7
7
 
8
8
 
9
- def get_llm_client() -> LLMClient:
9
+ def get_llm_client() -> LLMClientProtocol:
10
10
  match settings.llm.provider:
11
11
  case LLMProvider.OPENAI:
12
12
  return OpenAILLMClient()
@@ -6,10 +6,10 @@ from ai_review.clients.gemini.schema import (
6
6
  GeminiGenerationConfigSchema,
7
7
  )
8
8
  from ai_review.config import settings
9
- from ai_review.services.llm.types import LLMClient, ChatResultSchema
9
+ from ai_review.services.llm.types import LLMClientProtocol, ChatResultSchema
10
10
 
11
11
 
12
- class GeminiLLMClient(LLMClient):
12
+ class GeminiLLMClient(LLMClientProtocol):
13
13
  def __init__(self):
14
14
  self.http_client = get_gemini_http_client()
15
15
 
@@ -1,10 +1,10 @@
1
1
  from ai_review.clients.openai.client import get_openai_http_client
2
2
  from ai_review.clients.openai.schema import OpenAIChatRequestSchema, OpenAIMessageSchema
3
3
  from ai_review.config import settings
4
- from ai_review.services.llm.types import LLMClient, ChatResultSchema
4
+ from ai_review.services.llm.types import LLMClientProtocol, ChatResultSchema
5
5
 
6
6
 
7
- class OpenAILLMClient(LLMClient):
7
+ class OpenAILLMClient(LLMClientProtocol):
8
8
  def __init__(self):
9
9
  self.http_client = get_openai_http_client()
10
10
 
@@ -10,6 +10,6 @@ class ChatResultSchema(BaseModel):
10
10
  completion_tokens: int | None = None
11
11
 
12
12
 
13
- class LLMClient(Protocol):
13
+ class LLMClientProtocol(Protocol):
14
14
  async def chat(self, prompt: str, prompt_system: str) -> ChatResultSchema:
15
15
  ...
@@ -2,9 +2,10 @@ 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
4
  from ai_review.services.prompt.tools import normalize_prompt, format_file
5
+ from ai_review.services.prompt.types import PromptServiceProtocol
5
6
 
6
7
 
7
- class PromptService:
8
+ class PromptService(PromptServiceProtocol):
8
9
  @classmethod
9
10
  def prepare_prompt(cls, prompts: list[str], context: PromptContextSchema) -> str:
10
11
  prompt = "\n\n".join(prompts)
@@ -0,0 +1,27 @@
1
+ from typing import Protocol
2
+
3
+ from ai_review.services.diff.schema import DiffFileSchema
4
+ from ai_review.services.prompt.schema import PromptContextSchema
5
+
6
+
7
+ class PromptServiceProtocol(Protocol):
8
+ def prepare_prompt(self, prompts: list[str], context: PromptContextSchema) -> str:
9
+ ...
10
+
11
+ def build_inline_request(self, diff: DiffFileSchema, context: PromptContextSchema) -> str:
12
+ ...
13
+
14
+ def build_summary_request(self, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
15
+ ...
16
+
17
+ def build_context_request(self, diffs: list[DiffFileSchema], context: PromptContextSchema) -> str:
18
+ ...
19
+
20
+ def build_system_inline_request(self, context: PromptContextSchema) -> str:
21
+ ...
22
+
23
+ def build_system_context_request(self, context: PromptContextSchema) -> str:
24
+ ...
25
+
26
+ def build_system_summary_request(self, context: PromptContextSchema) -> str:
27
+ ...
@@ -0,0 +1,65 @@
1
+ from ai_review.config import settings
2
+ from ai_review.libs.asynchronous.gather import bounded_gather
3
+ from ai_review.libs.logger import get_logger
4
+ from ai_review.services.hook import hook
5
+ from ai_review.services.review.inline.schema import InlineCommentListSchema, InlineCommentSchema
6
+ from ai_review.services.review.summary.schema import SummaryCommentSchema
7
+ from ai_review.services.vcs.types import VCSClientProtocol
8
+
9
+ logger = get_logger("REVIEW_COMMENT_GATEWAY")
10
+
11
+
12
+ class ReviewCommentGateway:
13
+ def __init__(self, vcs: VCSClientProtocol):
14
+ self.vcs = vcs
15
+
16
+ async def has_existing_inline_comments(self) -> bool:
17
+ comments = await self.vcs.get_inline_comments()
18
+ has_comments = any(
19
+ settings.review.inline_tag in comment.body
20
+ for comment in comments
21
+ )
22
+ if has_comments:
23
+ logger.info("Skipping inline review: AI inline comments already exist")
24
+
25
+ return has_comments
26
+
27
+ async def has_existing_summary_comments(self) -> bool:
28
+ comments = await self.vcs.get_general_comments()
29
+ has_comments = any(
30
+ settings.review.summary_tag in comment.body for comment in comments
31
+ )
32
+ if has_comments:
33
+ logger.info("Skipping summary review: AI summary comment already exists")
34
+
35
+ return has_comments
36
+
37
+ async def process_inline_comment(self, comment: InlineCommentSchema):
38
+ try:
39
+ await hook.emit_inline_comment_start(comment)
40
+ await self.vcs.create_inline_comment(
41
+ file=comment.file,
42
+ line=comment.line,
43
+ message=comment.body_with_tag,
44
+ )
45
+ await hook.emit_inline_comment_complete(comment)
46
+ except Exception as error:
47
+ logger.exception(
48
+ f"Failed to process inline comment for {comment.file}:{comment.line} — {error}"
49
+ )
50
+ await hook.emit_inline_comment_error(comment)
51
+
52
+ logger.warning(f"Falling back to general comment for {comment.file}:{comment.line}")
53
+ await self.process_summary_comment(SummaryCommentSchema(text=comment.fallback_body))
54
+
55
+ async def process_summary_comment(self, comment: SummaryCommentSchema):
56
+ try:
57
+ await hook.emit_summary_comment_start(comment)
58
+ await self.vcs.create_general_comment(comment.body_with_tag)
59
+ await hook.emit_summary_comment_complete(comment)
60
+ except Exception as error:
61
+ logger.exception(f"Failed to process summary comment: {comment} — {error}")
62
+ await hook.emit_summary_comment_error(comment)
63
+
64
+ async def process_inline_comments(self, comments: InlineCommentListSchema) -> None:
65
+ await bounded_gather([self.process_inline_comment(comment) for comment in comments.root])
@@ -0,0 +1,40 @@
1
+ from ai_review.libs.logger import get_logger
2
+ from ai_review.services.artifacts.types import ArtifactsServiceProtocol
3
+ from ai_review.services.cost.types import CostServiceProtocol
4
+ from ai_review.services.hook import hook
5
+ from ai_review.services.llm.types import LLMClientProtocol
6
+
7
+ logger = get_logger("REVIEW_LLM_GATEWAY")
8
+
9
+
10
+ class ReviewLLMGateway:
11
+ def __init__(
12
+ self,
13
+ llm: LLMClientProtocol,
14
+ cost: CostServiceProtocol,
15
+ artifacts: ArtifactsServiceProtocol
16
+ ):
17
+ self.llm = llm
18
+ self.cost = cost
19
+ self.artifacts = artifacts
20
+
21
+ async def ask(self, prompt: str, prompt_system: str) -> str:
22
+ try:
23
+ await hook.emit_chat_start(prompt, prompt_system)
24
+ result = await self.llm.chat(prompt, prompt_system)
25
+ if not result.text:
26
+ logger.warning(
27
+ f"LLM returned an empty response (prompt length={len(prompt)} chars)"
28
+ )
29
+
30
+ report = self.cost.calculate(result)
31
+ if report:
32
+ logger.info(report.pretty())
33
+
34
+ await hook.emit_chat_complete(result, report)
35
+ await self.artifacts.save_llm_interaction(prompt, prompt_system, result.text)
36
+
37
+ return result.text
38
+ except Exception as error:
39
+ logger.exception(f"LLM request failed: {error}")
40
+ await hook.emit_chat_error(prompt, prompt_system)
@@ -38,8 +38,8 @@ class InlineCommentSchema(BaseModel):
38
38
  return f"{self.body}\n\n{settings.review.inline_tag}"
39
39
 
40
40
  @property
41
- def fallback_body_with_tag(self) -> str:
42
- return f"**{self.file}:{self.line}** — {self.message}\n\n{settings.review.inline_tag}"
41
+ def fallback_body(self) -> str:
42
+ return f"**{self.file}:{self.line}** — {self.message}"
43
43
 
44
44
 
45
45
  class InlineCommentListSchema(RootModel[list[InlineCommentSchema]]):