misterdev 0.2.0__py3-none-any.whl
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.
- misterdev/__init__.py +3 -0
- misterdev/agent.py +2166 -0
- misterdev/agent_helpers.py +194 -0
- misterdev/analyzers/__init__.py +0 -0
- misterdev/analyzers/project_analyzer/__init__.py +246 -0
- misterdev/analyzers/project_analyzer/detection.py +146 -0
- misterdev/analyzers/project_analyzer/merge.py +137 -0
- misterdev/analyzers/project_analyzer/overview.py +312 -0
- misterdev/analyzers/project_analyzer/prompts.py +83 -0
- misterdev/cli.py +370 -0
- misterdev/config.py +521 -0
- misterdev/core/__init__.py +0 -0
- misterdev/core/audit.py +88 -0
- misterdev/core/config.py +10 -0
- misterdev/core/context/__init__.py +0 -0
- misterdev/core/context/change_tracker.py +218 -0
- misterdev/core/context/contracts/__init__.py +63 -0
- misterdev/core/context/contracts/_log.py +9 -0
- misterdev/core/context/contracts/_text.py +12 -0
- misterdev/core/context/contracts/extraction.py +30 -0
- misterdev/core/context/contracts/python_generic.py +42 -0
- misterdev/core/context/contracts/registry.py +127 -0
- misterdev/core/context/contracts/rust_line.py +225 -0
- misterdev/core/context/contracts/rust_tree_sitter.py +141 -0
- misterdev/core/context/lsp.py +174 -0
- misterdev/core/context/scratchpad.py +111 -0
- misterdev/core/context/topography/__init__.py +43 -0
- misterdev/core/context/topography/_log.py +10 -0
- misterdev/core/context/topography/cache.py +91 -0
- misterdev/core/context/topography/engine.py +172 -0
- misterdev/core/context/topography/graph.py +675 -0
- misterdev/core/context/topography/nodes.py +55 -0
- misterdev/core/context/topography/parsers.py +95 -0
- misterdev/core/context/topography/syntax.py +54 -0
- misterdev/core/economics/__init__.py +0 -0
- misterdev/core/economics/context_budget.py +175 -0
- misterdev/core/economics/embeddings.py +232 -0
- misterdev/core/economics/free_models.py +108 -0
- misterdev/core/economics/llm_cache.py +105 -0
- misterdev/core/economics/model_catalog.py +79 -0
- misterdev/core/economics/model_ledger.py +331 -0
- misterdev/core/economics/model_selector.py +281 -0
- misterdev/core/execution/__init__.py +0 -0
- misterdev/core/execution/bounded.py +50 -0
- misterdev/core/execution/container.py +221 -0
- misterdev/core/execution/error_classifier.py +366 -0
- misterdev/core/execution/error_resolver.py +201 -0
- misterdev/core/execution/governance.py +283 -0
- misterdev/core/execution/outcomes.py +50 -0
- misterdev/core/execution/progress.py +120 -0
- misterdev/core/execution/project.py +231 -0
- misterdev/core/execution/registry.py +97 -0
- misterdev/core/execution/runtime.py +279 -0
- misterdev/core/gitcmd.py +39 -0
- misterdev/core/integration/__init__.py +0 -0
- misterdev/core/integration/mcp.py +368 -0
- misterdev/core/integration/mcp_gather.py +186 -0
- misterdev/core/models.py +35 -0
- misterdev/core/modes.py +184 -0
- misterdev/core/planning/__init__.py +0 -0
- misterdev/core/planning/advisor.py +89 -0
- misterdev/core/planning/assessment.py +135 -0
- misterdev/core/planning/decomposer.py +387 -0
- misterdev/core/planning/metacognition.py +103 -0
- misterdev/core/planning/sovereign.py +308 -0
- misterdev/core/planning/targets.py +201 -0
- misterdev/core/reporting/__init__.py +0 -0
- misterdev/core/reporting/report.py +377 -0
- misterdev/core/reporting/report_view.py +151 -0
- misterdev/core/task.py +163 -0
- misterdev/core/verification/__init__.py +0 -0
- misterdev/core/verification/claim_verifier.py +210 -0
- misterdev/core/verification/critic.py +324 -0
- misterdev/core/verification/gatekeeper/__init__.py +631 -0
- misterdev/core/verification/gatekeeper/constants.py +138 -0
- misterdev/core/verification/gatekeeper/helpers.py +28 -0
- misterdev/core/verification/goal_check.py +219 -0
- misterdev/core/verification/independent.py +68 -0
- misterdev/core/verification/mutation_gate.py +221 -0
- misterdev/core/verification/preflight.py +95 -0
- misterdev/core/verification/spec_tests.py +175 -0
- misterdev/core/verification/validator.py +495 -0
- misterdev/core/verification/vision_verify.py +185 -0
- misterdev/core/verification/web_verify.py +408 -0
- misterdev/environments/__init__.py +0 -0
- misterdev/environments/base_env.py +18 -0
- misterdev/environments/container_env.py +87 -0
- misterdev/environments/venv_env.py +42 -0
- misterdev/llm/__init__.py +0 -0
- misterdev/llm/client/__init__.py +152 -0
- misterdev/llm/client/base.py +382 -0
- misterdev/llm/client/edits.py +70 -0
- misterdev/llm/client/embeddings.py +121 -0
- misterdev/llm/client/errors.py +134 -0
- misterdev/llm/client/providers.py +535 -0
- misterdev/llm/client/response.py +24 -0
- misterdev/llm/prompt_manager.py +82 -0
- misterdev/llm/responses/__init__.py +34 -0
- misterdev/llm/responses/apply.py +131 -0
- misterdev/llm/responses/json_extract.py +80 -0
- misterdev/llm/responses/models.py +43 -0
- misterdev/llm/responses/parsing.py +494 -0
- misterdev/logging_setup.py +20 -0
- misterdev/mcp_server.py +208 -0
- misterdev/nl_cli.py +149 -0
- misterdev/plugins.py +115 -0
- misterdev/py.typed +0 -0
- misterdev/task_executors/__init__.py +0 -0
- misterdev/task_executors/base_executor.py +10 -0
- misterdev/task_executors/markdown_plan_executor/__init__.py +90 -0
- misterdev/task_executors/markdown_plan_executor/commands_mixin.py +82 -0
- misterdev/task_executors/markdown_plan_executor/context_mixin.py +221 -0
- misterdev/task_executors/markdown_plan_executor/critic_spec_mixin.py +174 -0
- misterdev/task_executors/markdown_plan_executor/edits_mixin.py +251 -0
- misterdev/task_executors/markdown_plan_executor/execute_mixin.py +727 -0
- misterdev/task_executors/markdown_plan_executor/gates_mixin.py +203 -0
- misterdev/task_executors/markdown_plan_executor/git_mixin.py +219 -0
- misterdev/task_executors/markdown_plan_executor/helpers.py +521 -0
- misterdev/task_executors/markdown_plan_executor/llm_mixin.py +238 -0
- misterdev/task_executors/markdown_plan_executor/results_mixin.py +23 -0
- misterdev/tools/__init__.py +19 -0
- misterdev/tools/base_tool.py +14 -0
- misterdev/tools/command.py +75 -0
- misterdev/tools/file_io.py +69 -0
- misterdev/tools/formatter.py +26 -0
- misterdev/tools/git_tool.py +90 -0
- misterdev/utils/__init__.py +0 -0
- misterdev/utils/file_utils.py +169 -0
- misterdev/utils/process.py +23 -0
- misterdev-0.2.0.dist-info/METADATA +326 -0
- misterdev-0.2.0.dist-info/RECORD +136 -0
- misterdev-0.2.0.dist-info/WHEEL +5 -0
- misterdev-0.2.0.dist-info/entry_points.txt +3 -0
- misterdev-0.2.0.dist-info/licenses/COMMERCIAL_LICENSE.md +34 -0
- misterdev-0.2.0.dist-info/licenses/LICENSE +661 -0
- misterdev-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from misterdev.config import get_section_setting, get_setting
|
|
5
|
+
from misterdev.logging_setup import setup_logger
|
|
6
|
+
|
|
7
|
+
from .providers import _deny_unless_training_allowed, _openrouter_sdk
|
|
8
|
+
|
|
9
|
+
logger = setup_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenRouterEmbeddingClient:
|
|
13
|
+
"""Embedding client over OpenRouter's OpenAI-compatible embeddings endpoint."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: dict, model: str):
|
|
16
|
+
llm_config = config.get("llm", {})
|
|
17
|
+
self.client, self.api_key = _openrouter_sdk(llm_config)
|
|
18
|
+
self.model = model
|
|
19
|
+
self.dimensions = get_section_setting("llm", llm_config, "embedding_dimensions")
|
|
20
|
+
self.data_collection = _deny_unless_training_allowed(llm_config)
|
|
21
|
+
|
|
22
|
+
def embed(self, texts: List[str]) -> List[List[float]]:
|
|
23
|
+
"""Return one vector per input text, in input order."""
|
|
24
|
+
kwargs = {
|
|
25
|
+
"model": self.model,
|
|
26
|
+
"input": texts,
|
|
27
|
+
"extra_body": {"provider": {"data_collection": self.data_collection}},
|
|
28
|
+
}
|
|
29
|
+
if self.dimensions:
|
|
30
|
+
kwargs["dimensions"] = self.dimensions
|
|
31
|
+
response = self.client.embeddings.create(**kwargs)
|
|
32
|
+
ordered = sorted(response.data, key=lambda d: d.index)
|
|
33
|
+
return [list(d.embedding) for d in ordered]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LocalEmbeddingClient:
|
|
37
|
+
"""Embedding client backed by a local fastembed model (offline, no API key).
|
|
38
|
+
|
|
39
|
+
The model is downloaded once and cached by fastembed, then runs on CPU via
|
|
40
|
+
ONNX. Loading is lazy (first ``embed`` call) so merely constructing the
|
|
41
|
+
client is cheap and failure (e.g. fastembed not installed) surfaces where
|
|
42
|
+
the factory can fall back gracefully.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, config: dict):
|
|
46
|
+
llm_config = config.get("llm", {})
|
|
47
|
+
self.model = (
|
|
48
|
+
get_section_setting("llm", llm_config, "local_embedding_model")
|
|
49
|
+
or "BAAI/bge-small-en-v1.5"
|
|
50
|
+
)
|
|
51
|
+
self._embedder = None
|
|
52
|
+
|
|
53
|
+
def _ensure(self):
|
|
54
|
+
if self._embedder is None:
|
|
55
|
+
from fastembed import TextEmbedding
|
|
56
|
+
|
|
57
|
+
self._embedder = TextEmbedding(model_name=self.model)
|
|
58
|
+
return self._embedder
|
|
59
|
+
|
|
60
|
+
def embed(self, texts: List[str]) -> List[List[float]]:
|
|
61
|
+
"""Return one vector per input text, in input order."""
|
|
62
|
+
embedder = self._ensure()
|
|
63
|
+
return [list(vec) for vec in embedder.embed(list(texts))]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _create_local_embedding_client(config: dict):
|
|
67
|
+
try:
|
|
68
|
+
return LocalEmbeddingClient(config)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.warning(f"Local embedding client unavailable: {e}")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _create_openrouter_embedding_client(config: dict):
|
|
75
|
+
if get_setting(config, "llm", "provider") != "openrouter":
|
|
76
|
+
return None
|
|
77
|
+
from misterdev.core.economics.embeddings import pick_embedding_model
|
|
78
|
+
|
|
79
|
+
model = pick_embedding_model(
|
|
80
|
+
get_setting(config, "llm", "embedding_model"),
|
|
81
|
+
prefer=get_setting(config, "llm", "embedding_prefer"),
|
|
82
|
+
)
|
|
83
|
+
if not model:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
return OpenRouterEmbeddingClient(config, model)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(f"Embedding client unavailable: {e}")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_embedding_client(config: dict):
|
|
93
|
+
"""Build an embedding client, or None when embeddings are unavailable.
|
|
94
|
+
|
|
95
|
+
Backend (llm.embedding_backend): "local" forces fastembed; "openrouter"
|
|
96
|
+
forces the API; "none" disables dense ranking; "auto" prefers a local
|
|
97
|
+
fastembed model when the extra is installed (free, offline, never blocked by
|
|
98
|
+
a provider data policy) and falls back to the OpenRouter API embedder only
|
|
99
|
+
when local isn't available. Any setup failure returns None, so retrieval
|
|
100
|
+
degrades to lexical-only rather than breaking.
|
|
101
|
+
"""
|
|
102
|
+
backend = get_setting(config, "llm", "embedding_backend")
|
|
103
|
+
if backend == "none":
|
|
104
|
+
return None
|
|
105
|
+
if backend == "local":
|
|
106
|
+
return _create_local_embedding_client(config)
|
|
107
|
+
if backend == "openrouter":
|
|
108
|
+
return _create_openrouter_embedding_client(config)
|
|
109
|
+
# auto
|
|
110
|
+
if get_setting(config, "llm", "provider") == "openrouter":
|
|
111
|
+
# Prefer a LOCAL fastembed model when the extra is installed: it is free,
|
|
112
|
+
# offline, and — unlike an OpenRouter embedding model under
|
|
113
|
+
# data_collection=deny — never blocked by a provider data policy (that
|
|
114
|
+
# block silently dropped both emathy runs to lexical-only ranking). When
|
|
115
|
+
# fastembed isn't installed, keep the API embedder (no regression).
|
|
116
|
+
if importlib.util.find_spec("fastembed") is not None:
|
|
117
|
+
local = _create_local_embedding_client(config)
|
|
118
|
+
if local is not None:
|
|
119
|
+
return local
|
|
120
|
+
return _create_openrouter_embedding_client(config)
|
|
121
|
+
return _create_local_embedding_client(config)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BudgetExceededError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LLMCallError(Exception):
|
|
9
|
+
def __init__(self, message: str, retryable: bool = False):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.retryable = retryable
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# HTTP status codes that denote a transient failure worth retrying / failing
|
|
15
|
+
# over. The 4xx members are the standard retryable ones (request timeout,
|
|
16
|
+
# conflict, too-early, rate limit); any other 4xx is a hard client error.
|
|
17
|
+
RETRYABLE_STATUS_CODES = frozenset({408, 409, 425, 429, 500, 502, 503, 504, 529})
|
|
18
|
+
|
|
19
|
+
# Exception class names (across the openai / anthropic / httpx SDKs) that always
|
|
20
|
+
# denote a transient failure, regardless of how the message happens to be
|
|
21
|
+
# phrased. Matched by bare class name so neither SDK must be importable here.
|
|
22
|
+
RETRYABLE_EXCEPTION_NAMES = frozenset(
|
|
23
|
+
{
|
|
24
|
+
"RateLimitError",
|
|
25
|
+
"APITimeoutError",
|
|
26
|
+
"APIConnectionError",
|
|
27
|
+
"APIConnectionTimeoutError",
|
|
28
|
+
"InternalServerError",
|
|
29
|
+
"ServiceUnavailableError",
|
|
30
|
+
"Timeout",
|
|
31
|
+
"TimeoutError",
|
|
32
|
+
"ConnectionError",
|
|
33
|
+
"ConnectTimeout",
|
|
34
|
+
"ReadTimeout",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Last-resort substring signals for plain exceptions that carry neither a status
|
|
39
|
+
# code nor a recognizable type (e.g. a provider that raises RuntimeError with a
|
|
40
|
+
# descriptive message). Checked only after the structured signals above.
|
|
41
|
+
RETRYABLE_ERROR_MARKERS = (
|
|
42
|
+
"rate limit",
|
|
43
|
+
"too many requests",
|
|
44
|
+
"timeout",
|
|
45
|
+
"timed out",
|
|
46
|
+
"temporarily unavailable",
|
|
47
|
+
"overloaded",
|
|
48
|
+
"connection",
|
|
49
|
+
"try again",
|
|
50
|
+
"429",
|
|
51
|
+
"500",
|
|
52
|
+
"502",
|
|
53
|
+
"503",
|
|
54
|
+
"504",
|
|
55
|
+
"529",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _error_status_code(error: Exception) -> Optional[int]:
|
|
60
|
+
"""Best-effort HTTP status code from a provider exception, or None.
|
|
61
|
+
|
|
62
|
+
Covers the openai/anthropic ``status_code`` attribute and the httpx-style
|
|
63
|
+
nested ``response.status_code``. ``bool`` is excluded so a stray ``True``
|
|
64
|
+
isn't read as code 1.
|
|
65
|
+
"""
|
|
66
|
+
for attr in ("status_code", "http_status"):
|
|
67
|
+
val = getattr(error, attr, None)
|
|
68
|
+
if isinstance(val, bool):
|
|
69
|
+
continue
|
|
70
|
+
if isinstance(val, int):
|
|
71
|
+
return val
|
|
72
|
+
if isinstance(val, str) and val.isdigit():
|
|
73
|
+
return int(val)
|
|
74
|
+
resp = getattr(error, "response", None)
|
|
75
|
+
sc = getattr(resp, "status_code", None)
|
|
76
|
+
if isinstance(sc, int) and not isinstance(sc, bool):
|
|
77
|
+
return sc
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _is_retryable_error(error: Exception) -> bool:
|
|
82
|
+
"""Classify an exception as transient (retry / fail over) or terminal.
|
|
83
|
+
|
|
84
|
+
Structured signals win: an explicit HTTP status code or a known transient
|
|
85
|
+
exception type is authoritative, so a hard 4xx (e.g. 400/401/404) is never
|
|
86
|
+
retried even if its message text coincidentally contains a retryable marker.
|
|
87
|
+
Only when no structured signal is present do we fall back to substrings.
|
|
88
|
+
"""
|
|
89
|
+
code = _error_status_code(error)
|
|
90
|
+
if code is not None:
|
|
91
|
+
if code in RETRYABLE_STATUS_CODES:
|
|
92
|
+
return True
|
|
93
|
+
if 400 <= code < 500:
|
|
94
|
+
return False
|
|
95
|
+
if code >= 500:
|
|
96
|
+
return True
|
|
97
|
+
if type(error).__name__ in RETRYABLE_EXCEPTION_NAMES:
|
|
98
|
+
return True
|
|
99
|
+
text = str(error).lower()
|
|
100
|
+
return any(marker in text for marker in RETRYABLE_ERROR_MARKERS)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _is_out_of_credits(error: Exception) -> bool:
|
|
104
|
+
"""True when a provider rejects the call for lack of funds (HTTP 402).
|
|
105
|
+
|
|
106
|
+
This is terminal and account-wide — retrying or failing over to another
|
|
107
|
+
model cannot fix it — so it is surfaced as budget exhaustion, not a generic
|
|
108
|
+
call error. Detected by the 402 status code, with a message fallback for
|
|
109
|
+
providers that only carry it in the text.
|
|
110
|
+
"""
|
|
111
|
+
if _error_status_code(error) == 402:
|
|
112
|
+
return True
|
|
113
|
+
text = str(error).lower()
|
|
114
|
+
return "insufficient credit" in text or (
|
|
115
|
+
"402" in text and ("credit" in text or "payment" in text)
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _api_error(provider: str, error: Exception) -> Exception:
|
|
120
|
+
"""Wrap a provider exception for the caller to raise.
|
|
121
|
+
|
|
122
|
+
An out-of-credits (402) response becomes a ``BudgetExceededError`` so the
|
|
123
|
+
run halts gracefully through the existing budget-halt path with an
|
|
124
|
+
actionable message, instead of crashing with a stack trace. Everything else
|
|
125
|
+
is an ``LLMCallError`` with retryability classified.
|
|
126
|
+
"""
|
|
127
|
+
if _is_out_of_credits(error):
|
|
128
|
+
return BudgetExceededError(
|
|
129
|
+
f"{provider} out of credits (HTTP 402): add credits to continue "
|
|
130
|
+
"(https://openrouter.ai/settings/credits)"
|
|
131
|
+
)
|
|
132
|
+
return LLMCallError(
|
|
133
|
+
f"{provider} API error: {error}", retryable=_is_retryable_error(error)
|
|
134
|
+
)
|