codespy-ai 2.0.0__tar.gz → 2.0.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.
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/PKG-INFO +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/pyproject.toml +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/__init__.py +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/context_safe.py +44 -1
- codespy_ai-2.0.2/src/codespy/agents/diagnostic_adapter.py +328 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/dspy_config.py +3 -3
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cerebral.py +163 -13
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/prefrontal.py +2 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/reach.py +46 -10
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/agent.py +11 -19
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/agent.py +5 -5
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/helpers.py +33 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/models.py +6 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/agent.py +21 -39
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/agent.py +13 -17
- codespy_ai-2.0.2/src/codespy/agents/rlm_tools.py +330 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config.py +13 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_dspy.py +1 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/pipeline.py +11 -1
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/LICENSE +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/README.md +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/cost_tracker.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/cost.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/delta_ops_compat.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/orphans.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/routines.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/cerebral/structured_output.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/budget.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/cartographer.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/context_memory.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/distiller.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/episode.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/hippocampus/hippocampus.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/pg0_manager.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/postgres.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/facets.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/format.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/prefrontal/query.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/memory/recall.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/audit/agent.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/code_review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/doc/doc_extractor.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/manifest_parser.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/scope/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/summary/agent.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/agents/review/supply_chain/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/cli.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/cli_local.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/cli_mcp_server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/cli_remote.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_git.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_io.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_llm.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_memory.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/config_utils.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/cyber/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/cyber/osv/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/github_client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/gitlab_client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/local_diff.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/git/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/mcp_utils.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/ripgrep/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/base_extractor.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/bash.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/cpp.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/csharp.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/go.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/java.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/javascript.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/kotlin.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/objc.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/php.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/python.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ripgrep_fallback.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/ruby.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/rust.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/swift.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/extractors/terraform.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/parser.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/parsers/treesitter/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/filesystem/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/storage/s3/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/web/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/web/client.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/web/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/tools/web/server.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/models.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/__init__.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/base.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/git.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/src/codespy/workflows/review/reporters/stdout.py +0 -0
- {codespy_ai-2.0.0 → codespy_ai-2.0.2}/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)
|