codespy-ai 0.2.0__tar.gz → 0.2.2__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 (68) hide show
  1. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/PKG-INFO +33 -2
  2. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/README.md +32 -1
  3. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/pyproject.toml +1 -1
  4. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/__init__.py +1 -1
  5. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/reviewer.py +46 -26
  6. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/config.py +16 -25
  7. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/config_llm.py +40 -1
  8. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/LICENSE +0 -0
  9. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/__init__.py +0 -0
  10. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/cost_tracker.py +0 -0
  11. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/dspy_config.py +0 -0
  12. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/__init__.py +0 -0
  13. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/models.py +0 -0
  14. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/__init__.py +0 -0
  15. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/code_and_doc_reviewer.py +0 -0
  16. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/deduplicator.py +0 -0
  17. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/helpers.py +0 -0
  18. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/scope_identifier.py +0 -0
  19. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/modules/supply_chain_auditor.py +0 -0
  20. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/reporters/__init__.py +0 -0
  21. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/reporters/base.py +0 -0
  22. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/reporters/git.py +0 -0
  23. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/agents/reviewer/reporters/stdout.py +0 -0
  24. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/cli.py +0 -0
  25. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/config_dspy.py +0 -0
  26. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/config_git.py +0 -0
  27. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/config_io.py +0 -0
  28. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/__init__.py +0 -0
  29. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/cyber/__init__.py +0 -0
  30. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
  31. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/cyber/osv/client.py +0 -0
  32. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/cyber/osv/models.py +0 -0
  33. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/cyber/osv/server.py +0 -0
  34. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/filesystem/__init__.py +0 -0
  35. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/filesystem/client.py +0 -0
  36. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/filesystem/models.py +0 -0
  37. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/filesystem/server.py +0 -0
  38. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/__init__.py +0 -0
  39. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/base.py +0 -0
  40. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/client.py +0 -0
  41. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/github_client.py +0 -0
  42. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/gitlab_client.py +0 -0
  43. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/models.py +0 -0
  44. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/git/server.py +0 -0
  45. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/mcp_utils.py +0 -0
  46. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/__init__.py +0 -0
  47. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
  48. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
  49. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
  50. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
  51. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
  52. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
  53. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
  54. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
  55. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
  56. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
  57. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
  58. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
  59. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
  60. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
  61. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
  62. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
  63. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
  64. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
  65. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/web/__init__.py +0 -0
  66. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/web/client.py +0 -0
  67. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/web/models.py +0 -0
  68. {codespy_ai-0.2.0 → codespy_ai-0.2.2}/src/codespy/tools/web/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespy-ai
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Code review agent powered by DSPy
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -247,13 +247,44 @@ docker run --rm \
247
247
  -e GITHUB_TOKEN=$GITHUB_TOKEN \
248
248
  -e DEFAULT_MODEL=claude-opus-4-6 \
249
249
  -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
250
- ghcr.io/khezen/codespy:0.1.0 review https://github.com/owner/repo/pull/123
250
+ ghcr.io/khezen/codespy:0.2.1 review https://github.com/owner/repo/pull/123
251
251
  ```
252
252
 
253
253
  ### GitHub Action
254
254
 
255
255
  Add CodeSpy to your repository for automatic PR reviews:
256
256
 
257
+ **Trigger on `@codespy review` comment:**
258
+
259
+ ```yaml
260
+ # .github/workflows/codespy-review.yml
261
+ name: CodeSpy Code Review
262
+
263
+ on:
264
+ issue_comment:
265
+ types: [created]
266
+
267
+ jobs:
268
+ review:
269
+ # Only run on PR comments containing '@codespy review'
270
+ if: |
271
+ github.event.issue.pull_request &&
272
+ contains(github.event.comment.body, '@codespy review')
273
+ runs-on: ubuntu-latest
274
+ permissions:
275
+ contents: read
276
+ pull-requests: write
277
+
278
+ steps:
279
+ - name: Run CodeSpy Review
280
+ uses: khezen/codespy@v1
281
+ with:
282
+ model: 'claude-opus-4-6'
283
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
284
+ ```
285
+
286
+ **Trigger automatically on every PR:**
287
+
257
288
  ```yaml
258
289
  # .github/workflows/codespy-review.yml
259
290
  name: CodeSpy Code Review
@@ -196,13 +196,44 @@ docker run --rm \
196
196
  -e GITHUB_TOKEN=$GITHUB_TOKEN \
197
197
  -e DEFAULT_MODEL=claude-opus-4-6 \
198
198
  -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
199
- ghcr.io/khezen/codespy:0.1.0 review https://github.com/owner/repo/pull/123
199
+ ghcr.io/khezen/codespy:0.2.1 review https://github.com/owner/repo/pull/123
200
200
  ```
201
201
 
202
202
  ### GitHub Action
203
203
 
204
204
  Add CodeSpy to your repository for automatic PR reviews:
205
205
 
206
+ **Trigger on `@codespy review` comment:**
207
+
208
+ ```yaml
209
+ # .github/workflows/codespy-review.yml
210
+ name: CodeSpy Code Review
211
+
212
+ on:
213
+ issue_comment:
214
+ types: [created]
215
+
216
+ jobs:
217
+ review:
218
+ # Only run on PR comments containing '@codespy review'
219
+ if: |
220
+ github.event.issue.pull_request &&
221
+ contains(github.event.comment.body, '@codespy review')
222
+ runs-on: ubuntu-latest
223
+ permissions:
224
+ contents: read
225
+ pull-requests: write
226
+
227
+ steps:
228
+ - name: Run CodeSpy Review
229
+ uses: khezen/codespy@v1
230
+ with:
231
+ model: 'claude-opus-4-6'
232
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
233
+ ```
234
+
235
+ **Trigger automatically on every PR:**
236
+
206
237
  ```yaml
207
238
  # .github/workflows/codespy-review.yml
208
239
  name: CodeSpy Code Review
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "codespy-ai"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Code review agent powered by DSPy"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """codespy - Code review agent powered by DSPy."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.2"
@@ -1,5 +1,6 @@
1
1
  """Main review pipeline that orchestrates all review modules."""
2
2
 
3
+ import asyncio
3
4
  import logging
4
5
  from pathlib import Path
5
6
 
@@ -100,6 +101,40 @@ class ReviewPipeline(dspy.Module):
100
101
  owner_path = mr.repo_owner.replace("/", "_")
101
102
  return cache_dir / owner_path / mr.repo_name
102
103
 
104
+ async def _run_review_modules(
105
+ self,
106
+ scopes: list,
107
+ repo_path: Path,
108
+ module_names: list[str],
109
+ ) -> list[Issue]:
110
+ """Run review modules concurrently in a single event loop.
111
+
112
+ Uses asyncio.gather instead of dspy.Parallel to avoid the
113
+ multi-thread + multi-event-loop conflict that causes
114
+ 'cannot schedule new futures after shutdown' errors.
115
+
116
+ Args:
117
+ scopes: Identified scopes with changed files
118
+ repo_path: Path to the cloned repository
119
+ module_names: Names of modules (for error logging)
120
+
121
+ Returns:
122
+ Aggregated list of issues from all modules
123
+ """
124
+ tasks = [
125
+ self.code_and_doc_reviewer.aforward(scopes=scopes, repo_path=repo_path),
126
+ self.supply_chain_auditor.aforward(scopes=scopes, repo_path=repo_path),
127
+ ]
128
+ results = await asyncio.gather(*tasks, return_exceptions=True)
129
+
130
+ all_issues: list[Issue] = []
131
+ for i, result in enumerate(results):
132
+ if isinstance(result, Exception):
133
+ logger.error(f"{module_names[i]} failed: {result}")
134
+ elif result is not None:
135
+ all_issues.extend(result)
136
+ return all_issues
137
+
103
138
  def forward(self, mr_url: str, verify_model: bool = True) -> ReviewResult:
104
139
  """Run the complete review pipeline on a merge request."""
105
140
  self.cost_tracker.reset()
@@ -122,28 +157,13 @@ class ReviewPipeline(dspy.Module):
122
157
  if manifest.dependencies_changed:
123
158
  logger.info(f" Dependencies changed: Yes")
124
159
 
125
- # Build list of review modules (they check signature enabled status internally)
126
- all_issues: list[Issue] = []
127
- exec_pairs = [
128
- (self.code_and_doc_reviewer, {"scopes": scopes, "repo_path": repo_path}),
129
- (self.supply_chain_auditor, {"scopes": scopes, "repo_path": repo_path}),
130
- ]
160
+ # Run review modules concurrently via asyncio.gather
131
161
  module_names = ["code_and_doc_reviewer", "supply_chain_auditor"]
132
-
133
- logger.info(f"Running review modules in parallel: {', '.join(module_names)}...")
134
- # Execute in parallel with error handling
135
- if exec_pairs:
136
- parallel = dspy.Parallel(num_threads=len(exec_pairs), return_failed_examples=True, provide_traceback=True)
137
- results, failed_examples, exceptions = parallel(exec_pairs)
138
- # Log any failures
139
- for i, (failed, exc) in enumerate(zip(failed_examples, exceptions)):
140
- if failed is not None and exc is not None:
141
- logger.error(f"{module_names[i]} failed: {exc}")
142
- # Aggregate successful results
143
- for result in results:
144
- if result is not None:
145
- all_issues.extend(result)
146
- logger.info(f"Found {len(all_issues)} issues before deduplication")
162
+ logger.info(f"Running review modules concurrently: {', '.join(module_names)}...")
163
+ all_issues = asyncio.run(
164
+ self._run_review_modules(scopes, repo_path, module_names)
165
+ )
166
+ logger.info(f"Found {len(all_issues)} issues before deduplication")
147
167
 
148
168
  # Deduplicate issues across reviewers (deduplicator checks if enabled internally)
149
169
  if len(all_issues) > 1:
@@ -178,7 +198,7 @@ class ReviewPipeline(dspy.Module):
178
198
  recommendation = "NEEDS_DISCUSSION" if all_issues else "APPROVE"
179
199
  # Collect per-signature statistics
180
200
  signature_stats_list = self._collect_signature_stats()
181
-
201
+
182
202
  return ReviewResult(
183
203
  mr_number=mr.number,
184
204
  mr_title=mr.title,
@@ -197,13 +217,13 @@ class ReviewPipeline(dspy.Module):
197
217
 
198
218
  def _collect_signature_stats(self) -> list[SignatureStatsResult]:
199
219
  """Collect statistics from all signatures that executed.
200
-
220
+
201
221
  Returns:
202
222
  List of SignatureStatsResult for each signature that ran
203
223
  """
204
224
  stats_list: list[SignatureStatsResult] = []
205
225
  all_signature_stats = self.cost_tracker.get_all_signature_stats()
206
-
226
+
207
227
  for signature_name, stats in all_signature_stats.items():
208
228
  stats_list.append(SignatureStatsResult(
209
229
  name=signature_name,
@@ -212,5 +232,5 @@ class ReviewPipeline(dspy.Module):
212
232
  call_count=stats.call_count,
213
233
  duration_seconds=stats.duration_seconds,
214
234
  ))
215
-
216
- return stats_list
235
+
236
+ return stats_list
@@ -330,32 +330,23 @@ class Settings(BaseSettings):
330
330
 
331
331
  @model_validator(mode="after")
332
332
  def sync_llm_settings(self) -> "Settings":
333
- """Sync LLM settings between nested and flat fields."""
334
- # Sync from nested to flat
335
- if self.llm.openai_api_key:
336
- self.openai_api_key = self.llm.openai_api_key
337
- if self.llm.anthropic_api_key:
338
- self.anthropic_api_key = self.llm.anthropic_api_key
339
- if self.llm.gemini_api_key:
340
- self.gemini_api_key = self.llm.gemini_api_key
341
- if self.llm.aws_region:
342
- self.aws_region = self.llm.aws_region
343
- if self.llm.aws_access_key_id:
344
- self.aws_access_key_id = self.llm.aws_access_key_id
345
- if self.llm.aws_secret_access_key:
346
- self.aws_secret_access_key = self.llm.aws_secret_access_key
347
-
348
- # Sync from flat to nested
349
- if self.openai_api_key and not self.llm.openai_api_key:
350
- self.llm.openai_api_key = self.openai_api_key
351
- if self.anthropic_api_key and not self.llm.anthropic_api_key:
352
- self.llm.anthropic_api_key = self.anthropic_api_key
353
- if self.gemini_api_key and not self.llm.gemini_api_key:
354
- self.llm.gemini_api_key = self.gemini_api_key
355
-
356
- # Sync prompt caching setting
357
- self.enable_prompt_caching = self.llm.enable_prompt_caching
333
+ """Sync LLM settings between nested and flat fields.
358
334
 
335
+ Delegates to LLMConfig.sync_from_flat which enforces:
336
+ env vars (flat fields from pydantic-settings) > YAML/nested > defaults.
337
+ """
338
+ merged = self.llm.sync_from_flat(
339
+ openai_api_key=self.openai_api_key,
340
+ anthropic_api_key=self.anthropic_api_key,
341
+ gemini_api_key=self.gemini_api_key,
342
+ aws_region=self.aws_region,
343
+ aws_access_key_id=self.aws_access_key_id,
344
+ aws_secret_access_key=self.aws_secret_access_key,
345
+ )
346
+ # Propagate merged values back to flat fields
347
+ for field, value in merged.items():
348
+ if value is not None:
349
+ setattr(self, field, value)
359
350
  return self
360
351
 
361
352
  @model_validator(mode="after")
@@ -268,4 +268,43 @@ class LLMConfig(BaseModel):
268
268
  azure_api_version: str | None = None
269
269
 
270
270
  # Enable provider-side prompt caching (Anthropic, OpenAI, Bedrock, etc.)
271
- enable_prompt_caching: bool = True
271
+ enable_prompt_caching: bool = True
272
+
273
+ def sync_from_flat(
274
+ self,
275
+ *,
276
+ openai_api_key: str | None = None,
277
+ anthropic_api_key: str | None = None,
278
+ gemini_api_key: str | None = None,
279
+ aws_region: str | None = None,
280
+ aws_access_key_id: str | None = None,
281
+ aws_secret_access_key: str | None = None,
282
+ ) -> dict[str, object]:
283
+ """Sync flat settings into this LLMConfig and return values to propagate back.
284
+
285
+ Priority: flat fields (from env vars via pydantic-settings) > nested defaults.
286
+ Returns dict of field values that should be set on the parent Settings.
287
+ """
288
+ # Step 1: Flat → nested (env vars win over nested defaults)
289
+ if openai_api_key:
290
+ self.openai_api_key = openai_api_key
291
+ if anthropic_api_key:
292
+ self.anthropic_api_key = anthropic_api_key
293
+ if gemini_api_key:
294
+ self.gemini_api_key = gemini_api_key
295
+ if aws_region:
296
+ self.aws_region = aws_region
297
+ if aws_access_key_id:
298
+ self.aws_access_key_id = aws_access_key_id
299
+ if aws_secret_access_key:
300
+ self.aws_secret_access_key = aws_secret_access_key
301
+
302
+ # Step 2: Return merged values (nested fills gaps where flat is not set)
303
+ return {
304
+ "openai_api_key": openai_api_key or self.openai_api_key,
305
+ "anthropic_api_key": anthropic_api_key or self.anthropic_api_key,
306
+ "gemini_api_key": gemini_api_key or self.gemini_api_key,
307
+ "aws_access_key_id": aws_access_key_id or self.aws_access_key_id,
308
+ "aws_secret_access_key": aws_secret_access_key or self.aws_secret_access_key,
309
+ "enable_prompt_caching": self.enable_prompt_caching,
310
+ }
File without changes