codespy-ai 2.0.0__tar.gz → 2.0.1__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.
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/PKG-INFO +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/pyproject.toml +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/__init__.py +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/context_safe.py +44 -1
- codespy_ai-2.0.1/src/codespy/agents/diagnostic_adapter.py +328 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/dspy_config.py +3 -3
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/code_review/agent.py +11 -19
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/doc/agent.py +5 -5
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/helpers.py +33 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/models.py +6 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/scope/agent.py +6 -10
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/supply_chain/agent.py +13 -17
- codespy_ai-2.0.1/src/codespy/agents/rlm_tools.py +330 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config.py +13 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_dspy.py +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/pipeline.py +11 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/LICENSE +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/README.md +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/cerebral.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/cost.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/routines.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/cerebral/structured_output.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/format.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/prefrontal.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/query.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/prefrontal/reach.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/memory/recall.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/audit/agent.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/summary/agent.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/cli.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/cli_local.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_git.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_io.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_llm.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_memory.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/config_utils.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.1}/src/codespy/workflows/review/server.py +0 -0
|
@@ -4,6 +4,7 @@ Provides transparent fallback to RLM (Recursive Language Model) when inputs
|
|
|
4
4
|
exceed the model's context window. Used to wrap all DSPy signature modules.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import enum
|
|
7
8
|
import logging
|
|
8
9
|
import re
|
|
9
10
|
|
|
@@ -19,6 +20,47 @@ _RE_CONTEXT_LENGTH = re.compile(r"maximum context length is \d+ tokens", re.IGNO
|
|
|
19
20
|
# Below this, context rot is not a concern regardless of ratio.
|
|
20
21
|
_MIN_RLM_THRESHOLD_TOKENS = 8192
|
|
21
22
|
|
|
23
|
+
# Replaces dspy.RLM's stock `reasoning` desc ("Think step-by-step: what do you
|
|
24
|
+
# know? What remains? Plan your next action."), which on its own triggers Bedrock
|
|
25
|
+
# content_filter on Claude Opus 5.5.
|
|
26
|
+
RLM_REASONING_DESC = "Short plan for the next step."
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _plain(value): # type: ignore[no-untyped-def]
|
|
30
|
+
"""Replace Enum members with their values, recursively.
|
|
31
|
+
|
|
32
|
+
dspy's PythonInterpreter injects inputs as `name = repr(value)`; an Enum repr
|
|
33
|
+
(`<IssueCategory.BUG: 'bug'>`) is not valid Python, so every REPL execution
|
|
34
|
+
fails with an empty SyntaxError.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(value, enum.Enum):
|
|
37
|
+
return value.value
|
|
38
|
+
if isinstance(value, (list, tuple, set)):
|
|
39
|
+
return [_plain(v) for v in value]
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return {_plain(k): _plain(v) for k, v in value.items()}
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CodespyRLM(dspy.RLM):
|
|
46
|
+
"""dspy.RLM adjusted for codespy.
|
|
47
|
+
|
|
48
|
+
- Neutral action-signature `reasoning` description (Bedrock content_filter).
|
|
49
|
+
- Enum inputs passed as plain values (REPL variable injection).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def forward(self, **kwargs): # type: ignore[no-untyped-def]
|
|
53
|
+
return super().forward(**{k: _plain(v) for k, v in kwargs.items()})
|
|
54
|
+
|
|
55
|
+
async def aforward(self, **kwargs): # type: ignore[no-untyped-def]
|
|
56
|
+
return await super().aforward(**{k: _plain(v) for k, v in kwargs.items()})
|
|
57
|
+
|
|
58
|
+
def _build_signatures(self): # type: ignore[no-untyped-def]
|
|
59
|
+
action_sig, extract_sig = super()._build_signatures()
|
|
60
|
+
if "reasoning" in action_sig.output_fields:
|
|
61
|
+
action_sig = action_sig.with_updated_fields("reasoning", desc=RLM_REASONING_DESC)
|
|
62
|
+
return action_sig, extract_sig
|
|
63
|
+
|
|
22
64
|
|
|
23
65
|
def estimate_context_overflow(model: str, max_tokens: int, input_text: str) -> bool:
|
|
24
66
|
"""Estimate whether input + max_tokens would exceed the model's context window.
|
|
@@ -197,9 +239,10 @@ class ContextSafe(dspy.Module):
|
|
|
197
239
|
|
|
198
240
|
def _create_rlm_fallback(self) -> dspy.RLM:
|
|
199
241
|
"""Create RLM with current signature and tools."""
|
|
200
|
-
return
|
|
242
|
+
return CodespyRLM(
|
|
201
243
|
self._get_current_signature(),
|
|
202
244
|
tools=self._tools,
|
|
203
245
|
max_iters=self._max_iters,
|
|
204
246
|
max_llm_calls=self._max_llm_calls,
|
|
247
|
+
verbose=logger.isEnabledFor(logging.DEBUG),
|
|
205
248
|
)
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Diagnostic adapter for logging empty LM response details.
|
|
2
|
+
|
|
3
|
+
Wraps TwoStepAdapter to capture diagnostics when the main LM returns an empty
|
|
4
|
+
or null response, which helps diagnose why Claude occasionally returns empty
|
|
5
|
+
text with hidden thinking content.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import dspy # type: ignore[import-untyped]
|
|
15
|
+
from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped]
|
|
16
|
+
from dspy.adapters.base import AdapterParseError # type: ignore[import-untyped]
|
|
17
|
+
|
|
18
|
+
from codespy.config import get_settings
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _getattr_or_key(obj: Any, name: str, default: Any = None) -> Any:
|
|
24
|
+
"""Get attribute or dict key, tolerating both access patterns."""
|
|
25
|
+
if hasattr(obj, name):
|
|
26
|
+
return getattr(obj, name, default)
|
|
27
|
+
if isinstance(obj, dict):
|
|
28
|
+
return obj.get(name, default)
|
|
29
|
+
return default
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _safe_repr(value: Any, max_len: int = 500) -> str:
|
|
33
|
+
"""Safe repr with length limit."""
|
|
34
|
+
try:
|
|
35
|
+
s = repr(value)
|
|
36
|
+
if len(s) > max_len:
|
|
37
|
+
return s[:max_len] + f"... ({len(s)} chars total)"
|
|
38
|
+
return s
|
|
39
|
+
except Exception:
|
|
40
|
+
return "<repr failed>"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_finish_reason(entry: dict) -> str:
|
|
44
|
+
"""Extract finish_reason from history entry."""
|
|
45
|
+
resp = entry.get("response")
|
|
46
|
+
if resp is None:
|
|
47
|
+
return "unknown"
|
|
48
|
+
|
|
49
|
+
resp_class_name = type(resp).__name__
|
|
50
|
+
if resp_class_name == "Response":
|
|
51
|
+
# lm15 style Response object
|
|
52
|
+
return _getattr_or_key(resp, "finish_reason", "unknown")
|
|
53
|
+
else:
|
|
54
|
+
# Standard dict-style response
|
|
55
|
+
choices = _getattr_or_key(resp, "choices", [])
|
|
56
|
+
if choices and len(choices) > 0:
|
|
57
|
+
choice = choices[0]
|
|
58
|
+
return _getattr_or_key(choice, "finish_reason", "unknown")
|
|
59
|
+
return "unknown"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _write_refusal_dump(entry: dict, model: str, lm_kwargs: dict, signature_name: str) -> Path | None:
|
|
63
|
+
"""Write a refusal dump to disk for later analysis.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
entry: The history entry containing messages and response.
|
|
67
|
+
model: The model name.
|
|
68
|
+
lm_kwargs: The LM kwargs (will have API keys stripped).
|
|
69
|
+
signature_name: The signature name for the dump filename.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Path to the written dump file, or None if writing failed.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
settings = get_settings()
|
|
76
|
+
cache_dir = Path(settings.review.cache_dir).expanduser().resolve()
|
|
77
|
+
refusals_dir = cache_dir / "refusals"
|
|
78
|
+
refusals_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
|
|
80
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
|
|
81
|
+
safe_sig = signature_name.replace("/", "_").replace("\\", "_")[:50]
|
|
82
|
+
dump_path = refusals_dir / f"{timestamp}-{safe_sig}.json"
|
|
83
|
+
|
|
84
|
+
# Strip API keys from lm_kwargs
|
|
85
|
+
safe_kwargs = {}
|
|
86
|
+
for key, value in lm_kwargs.items():
|
|
87
|
+
lower_key = key.lower()
|
|
88
|
+
# Redact actual secret keys, but not legitimate settings like max_tokens
|
|
89
|
+
if lower_key.endswith("_key") or lower_key.endswith("_secret") or lower_key.endswith("_token") or lower_key == "api_key":
|
|
90
|
+
safe_kwargs[key] = "<redacted>"
|
|
91
|
+
else:
|
|
92
|
+
safe_kwargs[key] = value
|
|
93
|
+
|
|
94
|
+
dump = {
|
|
95
|
+
"model": model,
|
|
96
|
+
"lm_kwargs": safe_kwargs,
|
|
97
|
+
"messages": entry.get("messages", []),
|
|
98
|
+
"response": entry.get("response"),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
with open(dump_path, "w", encoding="utf-8") as f:
|
|
102
|
+
json.dump(dump, f, indent=2, default=str)
|
|
103
|
+
|
|
104
|
+
logger.warning("Empty LM response: refusal dump written to %s", dump_path)
|
|
105
|
+
return dump_path
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug("Empty LM response: failed to write refusal dump: %s", e)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _log_empty_response(lm: Any, signature: Any) -> None:
|
|
112
|
+
"""Log detailed diagnostics for an empty LM response.
|
|
113
|
+
|
|
114
|
+
Wraps everything in try/except so diagnostics can never mask the original error.
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
model = getattr(lm, "model", "unknown")
|
|
118
|
+
max_tokens = getattr(lm, "kwargs", {}).get("max_tokens") if hasattr(lm, "kwargs") else None
|
|
119
|
+
reasoning_effort = getattr(lm, "kwargs", {}).get("reasoning_effort") if hasattr(lm, "kwargs") else None
|
|
120
|
+
|
|
121
|
+
# Get output field names from signature
|
|
122
|
+
output_fields: list[str] = []
|
|
123
|
+
signature_name = "unknown"
|
|
124
|
+
if signature is not None:
|
|
125
|
+
try:
|
|
126
|
+
signature_name = getattr(signature, "__name__", type(signature).__name__)
|
|
127
|
+
if hasattr(signature, "output_fields"):
|
|
128
|
+
output_fields = list(getattr(signature, "output_fields", []))
|
|
129
|
+
elif hasattr(signature, "fields"):
|
|
130
|
+
# Try to identify output fields from signature fields
|
|
131
|
+
fields = getattr(signature, "fields", {})
|
|
132
|
+
if isinstance(fields, dict):
|
|
133
|
+
output_fields = [k for k, v in fields.items() if getattr(v, "is_input", False) is False]
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
logger.warning(
|
|
138
|
+
"Empty LM response: model=%s max_tokens=%s reasoning_effort=%s output_fields=%s",
|
|
139
|
+
model, max_tokens, reasoning_effort, output_fields
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Read from LM history
|
|
143
|
+
history = getattr(lm, "history", None)
|
|
144
|
+
if not history:
|
|
145
|
+
logger.warning("Empty LM response: no LM history available")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
entry = history[-1]
|
|
150
|
+
except IndexError:
|
|
151
|
+
logger.warning("Empty LM response: LM history is empty")
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if not isinstance(entry, dict):
|
|
155
|
+
logger.warning("Empty LM response: unexpected history entry type %s", type(entry).__name__)
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
# Log usage info
|
|
159
|
+
usage = entry.get("usage")
|
|
160
|
+
if usage:
|
|
161
|
+
prompt_tokens = _getattr_or_key(usage, "prompt_tokens") or _getattr_or_key(usage, "input_tokens", 0)
|
|
162
|
+
completion_tokens = _getattr_or_key(usage, "completion_tokens") or _getattr_or_key(usage, "output_tokens", 0)
|
|
163
|
+
reasoning_tokens = _getattr_or_key(usage, "reasoning_tokens", 0)
|
|
164
|
+
logger.warning(
|
|
165
|
+
"Empty LM response: usage prompt_tokens=%s completion_tokens=%s reasoning_tokens=%s",
|
|
166
|
+
prompt_tokens, completion_tokens, reasoning_tokens
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
logger.warning("Empty LM response: no usage info in history entry")
|
|
170
|
+
|
|
171
|
+
# Log response details
|
|
172
|
+
resp = entry.get("response")
|
|
173
|
+
if resp is None:
|
|
174
|
+
logger.warning("Empty LM response: no response object in history entry")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Handle lm15 Response object vs dict
|
|
178
|
+
resp_class_name = type(resp).__name__
|
|
179
|
+
if resp_class_name == "Response":
|
|
180
|
+
# lm15 style Response object
|
|
181
|
+
finish_reason = _getattr_or_key(resp, "finish_reason", "unknown")
|
|
182
|
+
message = _getattr_or_key(resp, "message", None)
|
|
183
|
+
parts_info: list[str] = []
|
|
184
|
+
if message and hasattr(message, "parts"):
|
|
185
|
+
parts = getattr(message, "parts", [])
|
|
186
|
+
parts_info = [type(p).__name__ for p in parts]
|
|
187
|
+
usage_obj = _getattr_or_key(resp, "usage", None)
|
|
188
|
+
logger.warning(
|
|
189
|
+
"Empty LM response: lm15 Response finish_reason=%s message_parts=%s usage=%s",
|
|
190
|
+
finish_reason, parts_info, usage_obj
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
# Standard dict-style response
|
|
194
|
+
choices = _getattr_or_key(resp, "choices", [])
|
|
195
|
+
if choices and len(choices) > 0:
|
|
196
|
+
choice = choices[0]
|
|
197
|
+
finish_reason = _getattr_or_key(choice, "finish_reason", "unknown")
|
|
198
|
+
message = _getattr_or_key(choice, "message", {})
|
|
199
|
+
content = _getattr_or_key(message, "content", "")
|
|
200
|
+
|
|
201
|
+
logger.warning(
|
|
202
|
+
"Empty LM response: finish_reason=%s content=%s",
|
|
203
|
+
finish_reason, _safe_repr(content, 500)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Check for reasoning_content (Anthropic)
|
|
207
|
+
reasoning_content = _getattr_or_key(message, "reasoning_content", None)
|
|
208
|
+
if reasoning_content:
|
|
209
|
+
rc_len = len(str(reasoning_content))
|
|
210
|
+
logger.warning(
|
|
211
|
+
"Empty LM response: reasoning_content len=%s first_300=%s",
|
|
212
|
+
rc_len, _safe_repr(str(reasoning_content)[:300], 300)
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
# Check for thinking_blocks
|
|
216
|
+
thinking_blocks = _getattr_or_key(message, "thinking_blocks", None)
|
|
217
|
+
if thinking_blocks:
|
|
218
|
+
block_types = [type(b).__name__ for b in thinking_blocks]
|
|
219
|
+
logger.warning(
|
|
220
|
+
"Empty LM response: thinking_blocks count=%s types=%s",
|
|
221
|
+
len(thinking_blocks), block_types
|
|
222
|
+
)
|
|
223
|
+
else:
|
|
224
|
+
logger.warning("Empty LM response: no reasoning_content or thinking_blocks")
|
|
225
|
+
|
|
226
|
+
# Check for tool_calls
|
|
227
|
+
tool_calls = _getattr_or_key(message, "tool_calls", None)
|
|
228
|
+
if tool_calls:
|
|
229
|
+
logger.warning("Empty LM response: tool_calls count=%s", len(tool_calls))
|
|
230
|
+
else:
|
|
231
|
+
logger.warning("Empty LM response: no tool_calls")
|
|
232
|
+
|
|
233
|
+
# Log provider_specific_fields if present
|
|
234
|
+
provider_fields = _getattr_or_key(message, "provider_specific_fields", None)
|
|
235
|
+
if provider_fields:
|
|
236
|
+
logger.warning("Empty LM response: provider_specific_fields keys=%s", list(provider_fields.keys()) if isinstance(provider_fields, dict) else type(provider_fields).__name__)
|
|
237
|
+
else:
|
|
238
|
+
logger.warning("Empty LM response: no choices in response")
|
|
239
|
+
|
|
240
|
+
# Log request size
|
|
241
|
+
messages = entry.get("messages", [])
|
|
242
|
+
request_size = len(str(messages))
|
|
243
|
+
logger.warning("Empty LM response: request_size_chars=%s", request_size)
|
|
244
|
+
|
|
245
|
+
# If finish_reason is content_filter, write a refusal dump
|
|
246
|
+
finish_reason = _get_finish_reason(entry)
|
|
247
|
+
if finish_reason == "content_filter":
|
|
248
|
+
lm_kwargs = getattr(lm, "kwargs", {}) if hasattr(lm, "kwargs") else {}
|
|
249
|
+
_write_refusal_dump(entry, model, lm_kwargs, signature_name)
|
|
250
|
+
|
|
251
|
+
except Exception as diag_exc:
|
|
252
|
+
# Never let diagnostics mask the original error
|
|
253
|
+
logger.debug("Empty LM response: diagnostic logging failed: %s", diag_exc, exc_info=True)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _get_finish_reason_from_lm(lm: Any) -> str | None:
|
|
257
|
+
"""Extract finish_reason from LM history entry."""
|
|
258
|
+
history = getattr(lm, "history", None)
|
|
259
|
+
if not history:
|
|
260
|
+
return None
|
|
261
|
+
try:
|
|
262
|
+
entry = history[-1]
|
|
263
|
+
if not isinstance(entry, dict):
|
|
264
|
+
return None
|
|
265
|
+
return _get_finish_reason(entry)
|
|
266
|
+
except Exception:
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class DiagnosticTwoStepAdapter(TwoStepAdapter):
|
|
271
|
+
"""TwoStepAdapter that logs diagnostics on empty LM responses.
|
|
272
|
+
|
|
273
|
+
Wraps the parent's __call__ and __acall__ to catch AdapterParseError for empty/null
|
|
274
|
+
responses and log detailed diagnostics before re-raising the original
|
|
275
|
+
exception unchanged.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def _check_and_log_empty_response(
|
|
279
|
+
self, exc: AdapterParseError, lm: dspy.LM, signature: Any
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Check if exception is for empty response and log diagnostics.
|
|
282
|
+
|
|
283
|
+
If finish_reason is content_filter, raises a more descriptive error.
|
|
284
|
+
"""
|
|
285
|
+
# Only log for empty/null response errors
|
|
286
|
+
if "empty or null response" in str(exc).lower():
|
|
287
|
+
_log_empty_response(lm, signature)
|
|
288
|
+
# If finish_reason is content_filter, raise a more descriptive error
|
|
289
|
+
finish_reason = _get_finish_reason_from_lm(lm)
|
|
290
|
+
if finish_reason == "content_filter":
|
|
291
|
+
raise AdapterParseError(
|
|
292
|
+
adapter_name=getattr(self, "__class__.__name__", "DiagnosticTwoStepAdapter"),
|
|
293
|
+
signature=signature,
|
|
294
|
+
lm_response="",
|
|
295
|
+
message="model refused (content_filter)",
|
|
296
|
+
) from exc
|
|
297
|
+
|
|
298
|
+
def __call__(
|
|
299
|
+
self,
|
|
300
|
+
lm: dspy.LM,
|
|
301
|
+
lm_kwargs: dict[str, Any],
|
|
302
|
+
signature: Any,
|
|
303
|
+
demos: list[dict[str, Any]],
|
|
304
|
+
inputs: dict[str, Any],
|
|
305
|
+
) -> dict[str, Any]:
|
|
306
|
+
"""Call parent adapter, logging diagnostics on empty response."""
|
|
307
|
+
try:
|
|
308
|
+
return super().__call__(lm, lm_kwargs, signature, demos, inputs)
|
|
309
|
+
except AdapterParseError as exc:
|
|
310
|
+
self._check_and_log_empty_response(exc, lm, signature)
|
|
311
|
+
# Always re-raise the original exception unchanged
|
|
312
|
+
raise
|
|
313
|
+
|
|
314
|
+
async def acall(
|
|
315
|
+
self,
|
|
316
|
+
lm: dspy.LM,
|
|
317
|
+
lm_kwargs: dict[str, Any],
|
|
318
|
+
signature: Any,
|
|
319
|
+
demos: list[dict[str, Any]],
|
|
320
|
+
inputs: dict[str, Any],
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""Async call parent adapter, logging diagnostics on empty response."""
|
|
323
|
+
try:
|
|
324
|
+
return await super().acall(lm, lm_kwargs, signature, demos, inputs)
|
|
325
|
+
except AdapterParseError as exc:
|
|
326
|
+
self._check_and_log_empty_response(exc, lm, signature)
|
|
327
|
+
# Always re-raise the original exception unchanged
|
|
328
|
+
raise
|
|
@@ -7,8 +7,8 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
import dspy # type: ignore[import-untyped]
|
|
9
9
|
import litellm # type: ignore[import-untyped]
|
|
10
|
-
from dspy.adapters.two_step_adapter import TwoStepAdapter # type: ignore[import-untyped]
|
|
11
10
|
|
|
11
|
+
from codespy.agents.diagnostic_adapter import DiagnosticTwoStepAdapter
|
|
12
12
|
from codespy.config import Settings, get_settings
|
|
13
13
|
from codespy.config_memory import MEMORY_CONSOLIDATION, MEMORY_MENTAL_MODELS, MEMORY_RECALL, MEMORY_RETAIN, REFLECTION_MODULES, LLMSettings
|
|
14
14
|
from codespy.config_utils import secret_value
|
|
@@ -216,7 +216,7 @@ def lm_context(name: str) -> LMScope:
|
|
|
216
216
|
llm_config = settings.get_llm_config(name)
|
|
217
217
|
lm = new_lm(settings, llm_config)
|
|
218
218
|
extraction_lm = new_extraction_lm(settings)
|
|
219
|
-
ctx = dspy.context(lm=lm, adapter=
|
|
219
|
+
ctx = dspy.context(lm=lm, adapter=DiagnosticTwoStepAdapter(extraction_lm))
|
|
220
220
|
return LMScope(ctx=ctx, lm=lm, extraction_lm=extraction_lm)
|
|
221
221
|
|
|
222
222
|
|
|
@@ -270,7 +270,7 @@ def configure_dspy(settings: Settings) -> None:
|
|
|
270
270
|
|
|
271
271
|
dspy.settings.configure(
|
|
272
272
|
lm=lm,
|
|
273
|
-
adapter=
|
|
273
|
+
adapter=DiagnosticTwoStepAdapter(extraction_lm), # TwoStepAdapter solves ChatAdapter parsing failures
|
|
274
274
|
)
|
|
275
275
|
|
|
276
276
|
# Enable memory-only caching for LLM calls (no disk caching)
|
|
@@ -14,14 +14,16 @@ from codespy.agents.context_safe import ContextSafe
|
|
|
14
14
|
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus
|
|
15
15
|
from codespy.agents.memory.hippocampus.episode import submit_episode_save
|
|
16
16
|
from codespy.agents.memory.prefrontal import with_prefrontal_memory
|
|
17
|
-
from codespy.agents.review.models import Issue, IssueCategory, ReviewContext
|
|
18
|
-
from codespy.agents.review.scope.models import ScopeResult
|
|
19
17
|
from codespy.agents.review.helpers import (
|
|
18
|
+
filter_by_confidence,
|
|
20
19
|
issues_to_markdown,
|
|
21
20
|
make_scope_relative,
|
|
22
21
|
resolve_scope_root,
|
|
23
22
|
restore_repo_paths,
|
|
24
23
|
)
|
|
24
|
+
from codespy.agents.review.models import Issue, IssueCategory, ReviewContext
|
|
25
|
+
from codespy.agents.review.scope.models import ScopeResult
|
|
26
|
+
from codespy.agents.rlm_tools import build_rlm_agent, log_rlm_outcome, run_rlm
|
|
25
27
|
from codespy.config import get_settings
|
|
26
28
|
from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal
|
|
27
29
|
from codespy.tools.mcp_utils import cleanup_mcp_contexts, connect_mcp_server
|
|
@@ -129,6 +131,7 @@ class CodeReviewSignature(dspy.Signature):
|
|
|
129
131
|
- description: ≤25 words, imperative tone, no filler ("Fix X", "Rename Y to Z")
|
|
130
132
|
- No polite or conversational language
|
|
131
133
|
- Do not populate code_snippet — use line numbers instead
|
|
134
|
+
- Always set confidence: ≥0.9 verified with tools; 0.7–0.9 strong evidence; <0.7 weak
|
|
132
135
|
"""
|
|
133
136
|
|
|
134
137
|
scope: ScopeResult = dspy.InputField(
|
|
@@ -282,15 +285,9 @@ class CodeReviewer(dspy.Module):
|
|
|
282
285
|
sig = with_prefrontal_memory(CodeReviewSignature) if review_context.prefrontal_memory else CodeReviewSignature
|
|
283
286
|
recall_tool = pf.recall_tool() if pf else None
|
|
284
287
|
agent_tools = [*tools, recall_tool] if recall_tool else tools
|
|
285
|
-
agent =
|
|
286
|
-
dspy.RLM(
|
|
287
|
-
sig,
|
|
288
|
-
tools=agent_tools,
|
|
289
|
-
max_iters=max_iters,
|
|
290
|
-
max_llm_calls=self._settings.get_max_llm_calls("code_review"),
|
|
291
|
-
),
|
|
288
|
+
agent = build_rlm_agent(
|
|
292
289
|
sig,
|
|
293
|
-
|
|
290
|
+
agent_tools,
|
|
294
291
|
name="code_review",
|
|
295
292
|
max_iters=max_iters,
|
|
296
293
|
max_llm_calls=self._settings.get_max_llm_calls("code_review"),
|
|
@@ -336,16 +333,11 @@ class CodeReviewer(dspy.Module):
|
|
|
336
333
|
initial_memory=scope_initial_memory,
|
|
337
334
|
topics=topics,
|
|
338
335
|
)
|
|
339
|
-
result = await agent
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
result = await run_rlm(agent, scope=scoped, categories=categories, **pf_kwargs)
|
|
337
|
+
log_rlm_outcome(result, "code_review", scope.subroot)
|
|
338
|
+
issues = filter_by_confidence(
|
|
339
|
+
result.issues, self._settings.review.min_confidence, "code_review", scope.subroot
|
|
343
340
|
)
|
|
344
|
-
issues = [
|
|
345
|
-
issue
|
|
346
|
-
for issue in (result.issues or [])
|
|
347
|
-
if issue.confidence >= self._settings.review.min_confidence
|
|
348
|
-
]
|
|
349
341
|
if hippo is not None:
|
|
350
342
|
await hippo.aobserve(result)
|
|
351
343
|
# Fire-and-forget background episode save
|
|
@@ -19,6 +19,7 @@ from codespy.agents.review.scope.models import ScopeResult
|
|
|
19
19
|
from codespy.agents.review.doc.doc_extractor import extract_documentation
|
|
20
20
|
from codespy.agents.review.helpers import (
|
|
21
21
|
build_patches,
|
|
22
|
+
filter_by_confidence,
|
|
22
23
|
issues_to_markdown,
|
|
23
24
|
make_scope_relative,
|
|
24
25
|
resolve_scope_root,
|
|
@@ -78,6 +79,7 @@ class DocReviewSignature(dspy.Signature):
|
|
|
78
79
|
- description: ≤25 words, imperative tone ("Update X section", "Add Y to README")
|
|
79
80
|
- Empty list if documentation is up to date. No approval text ("LGTM", "looks good")
|
|
80
81
|
- No polite or conversational language
|
|
82
|
+
- Always set confidence: ≥0.9 verified with tools; 0.7–0.9 strong evidence; <0.7 weak
|
|
81
83
|
"""
|
|
82
84
|
|
|
83
85
|
patches: str = dspy.InputField(
|
|
@@ -261,11 +263,9 @@ class DocReviewer(dspy.Module):
|
|
|
261
263
|
categories=[IssueCategory.DOCUMENTATION],
|
|
262
264
|
**pf_kwargs,
|
|
263
265
|
)
|
|
264
|
-
issues =
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if issue.confidence >= self._settings.review.min_confidence
|
|
268
|
-
]
|
|
266
|
+
issues = filter_by_confidence(
|
|
267
|
+
result.issues, self._settings.review.min_confidence, "doc", scope.subroot
|
|
268
|
+
)
|
|
269
269
|
if hippo is not None:
|
|
270
270
|
await hippo.aobserve(result)
|
|
271
271
|
# Fire-and-forget background episode save
|
|
@@ -267,3 +267,36 @@ def scope_package_names(scope: ScopeResult | Any) -> list[str]:
|
|
|
267
267
|
if manifest is not None and getattr(manifest, "package_name", None):
|
|
268
268
|
return [manifest.package_name]
|
|
269
269
|
return []
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def filter_by_confidence(
|
|
273
|
+
issues: list[Issue] | None,
|
|
274
|
+
min_confidence: float,
|
|
275
|
+
module: str,
|
|
276
|
+
scope_subroot: str,
|
|
277
|
+
) -> list[Issue]:
|
|
278
|
+
"""Filter issues by confidence threshold with logging.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
issues: Raw issues list (may be None)
|
|
282
|
+
min_confidence: Minimum confidence threshold (0.0-1.0)
|
|
283
|
+
module: Module name for logging (e.g., 'code_review')
|
|
284
|
+
scope_subroot: Scope identifier for logging
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Filtered issues with confidence >= min_confidence
|
|
288
|
+
"""
|
|
289
|
+
if issues is None:
|
|
290
|
+
logger.info("%s scope %s: 0 raw issues, 0 kept (min_confidence=%.2f)",
|
|
291
|
+
module, scope_subroot, min_confidence)
|
|
292
|
+
return []
|
|
293
|
+
|
|
294
|
+
raw_count = len(issues)
|
|
295
|
+
kept = [issue for issue in issues if issue.confidence >= min_confidence]
|
|
296
|
+
kept_count = len(kept)
|
|
297
|
+
|
|
298
|
+
logger.info(
|
|
299
|
+
"%s scope %s: %d raw issues, %d kept (min_confidence=%.2f)",
|
|
300
|
+
module, scope_subroot, raw_count, kept_count, min_confidence
|
|
301
|
+
)
|
|
302
|
+
return kept
|
|
@@ -128,7 +128,12 @@ class Issue(BaseModel):
|
|
|
128
128
|
cwe_id: str | None = Field(
|
|
129
129
|
default=None, description="CWE ID for security issues (e.g., CWE-79)"
|
|
130
130
|
)
|
|
131
|
-
confidence: float = Field(
|
|
131
|
+
confidence: float = Field(
|
|
132
|
+
default=0.8,
|
|
133
|
+
ge=0.0,
|
|
134
|
+
le=1.0,
|
|
135
|
+
description="Confidence score (0-1). ≥0.9 verified with tools; 0.7–0.9 strong evidence; <0.7 weak"
|
|
136
|
+
)
|
|
132
137
|
|
|
133
138
|
@property
|
|
134
139
|
def location(self) -> str:
|
|
@@ -18,13 +18,13 @@ import dspy # type: ignore[import-untyped]
|
|
|
18
18
|
from pydantic import BaseModel, Field
|
|
19
19
|
|
|
20
20
|
from codespy.agents import SignatureContext, get_cost_tracker
|
|
21
|
-
from codespy.agents.context_safe import ContextSafe
|
|
22
21
|
from codespy.agents.memory.hippocampus import ContextMemory, Hippocampus
|
|
23
22
|
from codespy.agents.memory.hippocampus.episode import submit_episode_save
|
|
24
23
|
from codespy.agents.memory.prefrontal import build_facets, with_prefrontal_memory
|
|
25
24
|
from codespy.agents.review.models import ReviewContext
|
|
26
25
|
from codespy.agents.review.scope.manifest_parser import extract_package_name
|
|
27
26
|
from codespy.agents.review.scope.models import PackageManifest, ScopeResult, ScopeType
|
|
27
|
+
from codespy.agents.rlm_tools import build_rlm_agent, log_rlm_outcome, run_rlm
|
|
28
28
|
from codespy.config import get_settings
|
|
29
29
|
from codespy.config_memory import get_cerebral, get_episode_store, get_prefrontal
|
|
30
30
|
from codespy.tools.git.client import get_client
|
|
@@ -1060,15 +1060,9 @@ class ScopeResolver(dspy.Module):
|
|
|
1060
1060
|
)
|
|
1061
1061
|
recall_tool = pf.recall_tool() if pf else None
|
|
1062
1062
|
agent_tools = [*tools, recall_tool] if recall_tool else tools
|
|
1063
|
-
agent =
|
|
1064
|
-
dspy.RLM(
|
|
1065
|
-
sig,
|
|
1066
|
-
tools=agent_tools,
|
|
1067
|
-
max_iters=max_iters,
|
|
1068
|
-
max_llm_calls=self._settings.get_max_llm_calls("scope"),
|
|
1069
|
-
),
|
|
1063
|
+
agent = build_rlm_agent(
|
|
1070
1064
|
sig,
|
|
1071
|
-
|
|
1065
|
+
agent_tools,
|
|
1072
1066
|
name="scope",
|
|
1073
1067
|
max_iters=max_iters,
|
|
1074
1068
|
max_llm_calls=self._settings.get_max_llm_calls("scope"),
|
|
@@ -1123,7 +1117,8 @@ class ScopeResolver(dspy.Module):
|
|
|
1123
1117
|
run_id=run_id,
|
|
1124
1118
|
initial_memory=scope_initial_memory,
|
|
1125
1119
|
)
|
|
1126
|
-
result = await
|
|
1120
|
+
result = await run_rlm(
|
|
1121
|
+
agent,
|
|
1127
1122
|
candidates=candidates_str,
|
|
1128
1123
|
orphan_files=[f.filename for f in orphans],
|
|
1129
1124
|
pr_title=pr.title or "No title",
|
|
@@ -1131,6 +1126,7 @@ class ScopeResolver(dspy.Module):
|
|
|
1131
1126
|
project_instructions=project_instructions,
|
|
1132
1127
|
**pf_kwargs,
|
|
1133
1128
|
)
|
|
1129
|
+
log_rlm_outcome(result, "scope", pr.repo_full_name)
|
|
1134
1130
|
if hippo is not None:
|
|
1135
1131
|
await hippo.aobserve(result)
|
|
1136
1132
|
|