argus-code-review 0.2.5__tar.gz → 0.2.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/CHANGELOG.md +14 -1
  2. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/PKG-INFO +2 -2
  3. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/pyproject.toml +1 -1
  4. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_openai_client.py +38 -0
  5. argus_code_review-0.2.6/tests/test_output_models.py +188 -0
  6. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/.gitignore +0 -0
  7. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/LICENSE +0 -0
  8. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/README.md +0 -0
  9. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/__init__.py +0 -0
  10. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/bench.py +0 -0
  11. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/bench_default.toml +0 -0
  12. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/cli.py +0 -0
  13. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/config.py +0 -0
  14. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/coverage.py +0 -0
  15. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/dotenv_utils.py +0 -0
  16. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/gemini_cache.py +0 -0
  17. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/gemini_runner.py +0 -0
  18. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/github_client.py +0 -0
  19. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/graph.py +0 -0
  20. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/helpers.py +0 -0
  21. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/llm/models.py +0 -0
  22. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/llm/output_models.py +0 -0
  23. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/llm/pricing.py +0 -0
  24. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/llm/usage.py +0 -0
  25. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/models.py +0 -0
  26. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/openai_client.py +0 -0
  27. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/openai_runner.py +0 -0
  28. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/pipeline_models.py +0 -0
  29. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/__init__.py +0 -0
  30. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/actions_scanner.py +0 -0
  31. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/engine.py +0 -0
  32. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/eslint_bundle/.gitignore +0 -0
  33. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/eslint_bundle/README.md +0 -0
  34. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/eslint_bundle/eslint.config.js +0 -0
  35. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/eslint_bundle/package-lock.json +0 -0
  36. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/eslint_bundle/package.json +0 -0
  37. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/js_scanner.py +0 -0
  38. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/migration_scanner.py +0 -0
  39. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/rules/README.md +0 -0
  40. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/sarif.py +0 -0
  41. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/scanner_utils.py +0 -0
  42. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/secrets_scanner.py +0 -0
  43. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/shadow.py +0 -0
  44. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/terraform_scanner.py +0 -0
  45. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/precheck/workflow_lint_scanner.py +0 -0
  46. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/__init__.py +0 -0
  47. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-blocking-validator.md +0 -0
  48. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-coverage-check.md +0 -0
  49. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-cross-cutting.md +0 -0
  50. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-feedback-verifier.md +0 -0
  51. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-lite.md +0 -0
  52. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-planner.md +0 -0
  53. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-preflight-router.md +0 -0
  54. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-prior-art.md +0 -0
  55. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-deployment.md +0 -0
  56. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-frontend.md +0 -0
  57. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-infra.md +0 -0
  58. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-llm-patterns.md +0 -0
  59. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-observability.md +0 -0
  60. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-orchestration.md +0 -0
  61. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-security.md +0 -0
  62. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-slackbot.md +0 -0
  63. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-specialist-sql.md +0 -0
  64. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-subagent.md +0 -0
  65. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-tests-and-docs.md +0 -0
  66. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts/pr-review-writer.md +0 -0
  67. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/prompts_runtime.py +0 -0
  68. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/repo_provision.py +0 -0
  69. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/review_tools.py +0 -0
  70. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/runners.py +0 -0
  71. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/__init__.py +0 -0
  72. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/http.py +0 -0
  73. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/models.py +0 -0
  74. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/precheck.py +0 -0
  75. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/resolver.py +0 -0
  76. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/session.py +0 -0
  77. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/sql.py +0 -0
  78. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/argus/storage/sqlite.py +0 -0
  79. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/008_add_code_reviews.sql +0 -0
  80. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/009_add_reviewer_version.sql +0 -0
  81. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/010_add_review_patterns.sql +0 -0
  82. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/011_add_review_progress_columns.sql +0 -0
  83. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/015_create_agent_runs.sql +0 -0
  84. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/016_add_agent_runs_failure_reason.sql +0 -0
  85. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/017_add_precheck_rules.sql +0 -0
  86. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/schema/018_widen_agent_runs_failure_reason.sql +0 -0
  87. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/__init__.py +0 -0
  88. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/conftest.py +0 -0
  89. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/golden/review_response.schema.json +0 -0
  90. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/__init__.py +0 -0
  91. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_backend_contract.py +0 -0
  92. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_http.py +0 -0
  93. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_models.py +0 -0
  94. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_resolver.py +0 -0
  95. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_session.py +0 -0
  96. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_sql.py +0 -0
  97. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/storage/test_sqlite_backend.py +0 -0
  98. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_actions_scanner.py +0 -0
  99. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_argus_review_local.py +0 -0
  100. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_bench.py +0 -0
  101. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_bench_config_guard.py +0 -0
  102. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_catchup_gate.py +0 -0
  103. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_cli_args.py +0 -0
  104. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_cli_output_contract.py +0 -0
  105. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_cli_post_review.py +0 -0
  106. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_cli_preflight.py +0 -0
  107. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_cli_prompts.py +0 -0
  108. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_config.py +0 -0
  109. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_conftest.py +0 -0
  110. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_gemini_cache.py +0 -0
  111. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_gemini_runner.py +0 -0
  112. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_github_client_checks_signal.py +0 -0
  113. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_github_client_write.py +0 -0
  114. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_fetch_diff.py +0 -0
  115. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_get_llm_temperature.py +0 -0
  116. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_http_guards.py +0 -0
  117. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_precheck.py +0 -0
  118. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_preflight_image_bump.py +0 -0
  119. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_progress.py +0 -0
  120. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_storage_resolution.py +0 -0
  121. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_graph_timeout_surfacing.py +0 -0
  122. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_js_scanner.py +0 -0
  123. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_llm_models.py +0 -0
  124. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_llm_models_override.py +0 -0
  125. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_llm_pricing.py +0 -0
  126. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_migration_scanner.py +0 -0
  127. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_models.py +0 -0
  128. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_multi_round.py +0 -0
  129. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_openai_runner.py +0 -0
  130. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_packaging.py +0 -0
  131. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_plan_review.py +0 -0
  132. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_precheck_engine.py +0 -0
  133. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_precheck_engine_integration.py +0 -0
  134. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_precheck_sarif.py +0 -0
  135. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_precheck_shadow.py +0 -0
  136. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_precheck_shadow_integration.py +0 -0
  137. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_prompts.py +0 -0
  138. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_repo_provision.py +0 -0
  139. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_review_patterns_integration.py +0 -0
  140. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_review_tools.py +0 -0
  141. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_runners_context7.py +0 -0
  142. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_runners_context_usage.py +0 -0
  143. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_runners_helpers.py +0 -0
  144. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_runners_new.py +0 -0
  145. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_scanner_utils.py +0 -0
  146. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_secrets_scanner.py +0 -0
  147. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_specialist_validation.py +0 -0
  148. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_stage_costs.py +0 -0
  149. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_storage_precheck.py +0 -0
  150. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_terraform_scanner.py +0 -0
  151. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_terraform_scanner_integration.py +0 -0
  152. {argus_code_review-0.2.5 → argus_code_review-0.2.6}/tests/test_workflow_lint_scanner.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.6] - 2026-09-19
11
+
12
+ ### Fixed
13
+
14
+ - Bounded `openai` dependency to `>=1.66.0,<2` in `pyproject.toml` (TECH-6590) to
15
+ prevent unconstrained fresh installs from resolving to a future breaking major
16
+ release. The floor was also tightened from `>=1.50.0` to `>=1.66.0` where the
17
+ OpenAI Responses API (`client.responses.create()`) used by this codebase was
18
+ introduced. (Note: this is a general hygiene fix; it does not address the root
19
+ cause of the TECH-6590-reported crash, which was a separate shared-venv version-skew
20
+ issue tracked in TECH-6592.)
21
+
10
22
  ## [0.2.5] - 2026-09-18
11
23
 
12
24
  ### Added
@@ -326,7 +338,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
326
338
  packaged set.
327
339
  - `argus --version`, `argus prompts list`, and `argus prompts export`.
328
340
 
329
- [Unreleased]: https://github.com/redesignhealth/argus-review/compare/v0.2.5...HEAD
341
+ [Unreleased]: https://github.com/redesignhealth/argus-review/compare/v0.2.6...HEAD
342
+ [0.2.6]: https://github.com/redesignhealth/argus-review/compare/v0.2.5...v0.2.6
330
343
  [0.2.5]: https://github.com/redesignhealth/argus-review/compare/v0.2.4...v0.2.5
331
344
  [0.2.4]: https://github.com/redesignhealth/argus-review/compare/v0.2.3...v0.2.4
332
345
  [0.2.3]: https://github.com/redesignhealth/argus-review/compare/v0.2.2...v0.2.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argus-code-review
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Self-orchestrated PR review agent using LangGraph + Claude Agent SDK
5
5
  Project-URL: Repository, https://github.com/redesignhealth/argus-review
6
6
  Project-URL: Issues, https://github.com/redesignhealth/argus-review/issues
@@ -26,7 +26,7 @@ Requires-Dist: langgraph-checkpoint-sqlite>=2.0.0
26
26
  Requires-Dist: langgraph<2,>=1.0.10
27
27
  Requires-Dist: langsmith<1,>=0.2.0
28
28
  Requires-Dist: litellm<2.0.0,>=1.50.0
29
- Requires-Dist: openai>=1.50.0
29
+ Requires-Dist: openai<2,>=1.66.0
30
30
  Requires-Dist: psycopg-pool>=3.2.0
31
31
  Requires-Dist: psycopg[binary]>=3.2.0
32
32
  Requires-Dist: pydantic-settings>=2.0.0
@@ -46,7 +46,7 @@ dependencies = [
46
46
  # HTTP client (GitHub API, storage HTTP shim)
47
47
  "httpx>=0.27.0",
48
48
  # OpenAI SDK for the plan-extraction fallback path
49
- "openai>=1.50.0",
49
+ "openai>=1.66.0,<2",
50
50
  # Per-token model pricing table (argus/llm/pricing.py) -- single source
51
51
  # of truth for $/token rates, instead of a hand-maintained duplicate.
52
52
  "litellm>=1.50.0,<2.0.0",
@@ -80,3 +80,41 @@ class TestGetAsyncOpenAIClientBaseURL:
80
80
 
81
81
  mock_async_openai.assert_called_once()
82
82
  assert mock_async_openai.call_args.kwargs["base_url"] is None
83
+
84
+
85
+ class TestOpenAIClientSyncRespond:
86
+ def test_respond_passes_text_format_to_responses_create(self) -> None:
87
+ """text_format is wrapped in text={'format': text_format} for client.responses.create."""
88
+ settings = _make_settings(None)
89
+ mock_response = MagicMock()
90
+ mock_response.model = "gpt-5.4-mini"
91
+ mock_response.status = "completed"
92
+ mock_response.output = []
93
+ mock_response.output_text = "{}"
94
+
95
+ mock_client_instance = MagicMock()
96
+ mock_client_instance.responses.create.return_value = mock_response
97
+
98
+ text_format = {
99
+ "type": "json_schema",
100
+ "name": "test_schema",
101
+ "strict": True,
102
+ "schema": {"type": "object", "properties": {}},
103
+ }
104
+
105
+ with (
106
+ patch("argus.openai_client.get_settings", return_value=settings),
107
+ patch("argus.openai_client.OpenAI", return_value=mock_client_instance),
108
+ patch("argus.openai_client.wrap_openai", side_effect=lambda client: client),
109
+ ):
110
+ client = OpenAIClientSync()
111
+ resp = client.respond(
112
+ input="test prompt",
113
+ text_format=text_format,
114
+ )
115
+
116
+ mock_client_instance.responses.create.assert_called_once()
117
+ call_kwargs = mock_client_instance.responses.create.call_args.kwargs
118
+ assert call_kwargs["text"] == {"format": text_format}
119
+ assert call_kwargs["input"] == "test prompt"
120
+ assert resp == mock_response
@@ -0,0 +1,188 @@
1
+ """Tests for argus.llm.output_models: schema generation and Responses API format.
2
+
3
+ Verifies pydantic_to_response_format (including exclude= and strict= handling)
4
+ and LLMOutputModel schema generation for compatibility with OpenAI Responses API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Literal
10
+ from pydantic import BaseModel, Field
11
+
12
+ from argus.graph import _PIPELINE_ONLY_RESPONSE_FIELDS
13
+ from argus.llm.output_models import (
14
+ LLMOutputModel,
15
+ make_schema_strict,
16
+ pydantic_to_response_format,
17
+ )
18
+ from argus.models import ReviewResponse
19
+
20
+
21
+ class SampleModel(BaseModel):
22
+ name: str = Field(description="The name")
23
+ count: int = Field(default=0, description="The count")
24
+ tag: str | None = None
25
+
26
+
27
+ class SampleModelWithExcludedFields(BaseModel):
28
+ title: str
29
+ verdict: str
30
+ dropped_field_1: dict[str, float] = Field(default_factory=dict)
31
+ dropped_field_2: dict[str, float] = Field(default_factory=dict)
32
+
33
+
34
+ class SampleOutputModel(LLMOutputModel):
35
+ _schema_name = "custom_output"
36
+
37
+ summary: str = Field(description="Summary of results")
38
+ status: Literal["pass", "fail"] = Field(description="Pass or fail")
39
+
40
+
41
+ class TestPydanticToResponseFormat:
42
+ def test_default_name_and_strict(self) -> None:
43
+ """pydantic_to_response_format defaults name to model name in lowercase and strict=True."""
44
+ rf = pydantic_to_response_format(SampleModel)
45
+
46
+ assert rf["type"] == "json_schema"
47
+ assert rf["name"] == "samplemodel"
48
+ assert rf["strict"] is True
49
+ schema = rf["schema"]
50
+ assert schema["type"] == "object"
51
+ assert schema["additionalProperties"] is False
52
+ assert set(schema["required"]) == set(schema["properties"].keys())
53
+
54
+ def test_custom_name(self) -> None:
55
+ """pydantic_to_response_format respects custom name."""
56
+ rf = pydantic_to_response_format(SampleModel, name="my_custom_schema")
57
+ assert rf["name"] == "my_custom_schema"
58
+
59
+ def test_strict_false(self) -> None:
60
+ """pydantic_to_response_format respects strict=False."""
61
+ rf = pydantic_to_response_format(SampleModel, strict=False)
62
+ assert rf["strict"] is False
63
+ # strict=False does not enforce additionalProperties: false
64
+ assert rf["schema"].get("additionalProperties") is not False
65
+
66
+ def test_exclude_fields(self) -> None:
67
+ """exclude= removes specified fields from schema properties and required."""
68
+ rf = pydantic_to_response_format(
69
+ SampleModelWithExcludedFields,
70
+ name="filtered_model",
71
+ exclude={"dropped_field_1", "dropped_field_2"},
72
+ )
73
+
74
+ properties = rf["schema"]["properties"]
75
+ assert "title" in properties
76
+ assert "verdict" in properties
77
+ assert "dropped_field_1" not in properties
78
+ assert "dropped_field_2" not in properties
79
+
80
+ required = rf["schema"]["required"]
81
+ assert "title" in required
82
+ assert "verdict" in required
83
+ assert "dropped_field_1" not in required
84
+ assert "dropped_field_2" not in required
85
+
86
+ def test_review_response_with_pipeline_only_fields_excluded(self) -> None:
87
+ """ReviewResponse with _PIPELINE_ONLY_RESPONSE_FIELDS matches production graph.py usage."""
88
+ rf = pydantic_to_response_format(
89
+ ReviewResponse,
90
+ "review_response",
91
+ exclude=_PIPELINE_ONLY_RESPONSE_FIELDS,
92
+ )
93
+
94
+ assert rf["type"] == "json_schema"
95
+ assert rf["name"] == "review_response"
96
+ assert rf["strict"] is True
97
+
98
+ properties = rf["schema"]["properties"]
99
+ required = rf["schema"]["required"]
100
+
101
+ for excluded in _PIPELINE_ONLY_RESPONSE_FIELDS:
102
+ assert excluded not in properties
103
+ assert excluded not in required
104
+
105
+ # Essential ReviewResponse fields must remain present and required
106
+ assert "verdict" in properties
107
+ assert "verdict" in required
108
+ assert "risk_level" in properties
109
+ assert "risk_level" in required
110
+ assert "findings" in properties
111
+ assert "findings" in required
112
+
113
+
114
+ class TestLLMOutputModel:
115
+ def test_to_prompt_schema(self) -> None:
116
+ """to_prompt_schema generates markdown with field definitions and example."""
117
+ schema_text = SampleOutputModel.to_prompt_schema()
118
+ assert "Your output MUST be valid JSON" in schema_text
119
+ assert "| `summary` | string |" in schema_text
120
+ assert "| `status` | enum |" in schema_text
121
+ assert (
122
+ "custom_output" not in schema_text
123
+ ) # schema name is in API response_format, not prompt
124
+
125
+ def test_to_response_format(self) -> None:
126
+ """to_response_format produces Chat Completions nested format."""
127
+ rf = SampleOutputModel.to_response_format()
128
+ assert rf["type"] == "json_schema"
129
+ assert "json_schema" in rf
130
+ assert rf["json_schema"]["name"] == "custom_output"
131
+ assert rf["json_schema"]["strict"] is True
132
+
133
+ def test_to_response_format_flat(self) -> None:
134
+ """to_response_format_flat produces Responses API flat format."""
135
+ rf = SampleOutputModel.to_response_format_flat()
136
+ assert rf["type"] == "json_schema"
137
+ assert rf["name"] == "custom_output"
138
+ assert rf["strict"] is True
139
+ assert "schema" in rf
140
+ assert rf["schema"]["type"] == "object"
141
+ assert rf["schema"]["additionalProperties"] is False
142
+
143
+
144
+ class TestMakeSchemaStrict:
145
+ def test_recursively_sets_additional_properties_false_and_required(self) -> None:
146
+ """make_schema_strict sets additionalProperties=False and required on all objects."""
147
+ schema = {
148
+ "type": "object",
149
+ "properties": {
150
+ "user": {
151
+ "type": "object",
152
+ "properties": {
153
+ "name": {"type": "string"},
154
+ },
155
+ },
156
+ "items": {
157
+ "type": "array",
158
+ "items": {
159
+ "type": "object",
160
+ "properties": {
161
+ "id": {"type": "integer"},
162
+ },
163
+ },
164
+ },
165
+ },
166
+ }
167
+
168
+ strict_schema = make_schema_strict(schema)
169
+
170
+ assert strict_schema["additionalProperties"] is False
171
+ assert set(strict_schema["required"]) == {"user", "items"}
172
+
173
+ user_prop = strict_schema["properties"]["user"]
174
+ assert user_prop["additionalProperties"] is False
175
+ assert user_prop["required"] == ["name"]
176
+
177
+ item_schema = strict_schema["properties"]["items"]["items"]
178
+ assert item_schema["additionalProperties"] is False
179
+ assert item_schema["required"] == ["id"]
180
+
181
+ def test_ref_clears_sibling_keywords(self) -> None:
182
+ """OpenAI strict mode requires $ref to have no sibling keywords."""
183
+ schema = {
184
+ "$ref": "#/$defs/SomeType",
185
+ "description": "Sibling keyword that OpenAI rejects in strict mode",
186
+ }
187
+ strict_schema = make_schema_strict(schema)
188
+ assert strict_schema == {"$ref": "#/$defs/SomeType"}