codecortex-context-engine 0.1.0a1__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.
- codecortex/__init__.py +3 -0
- codecortex/architecture/__init__.py +23 -0
- codecortex/architecture/drift.py +182 -0
- codecortex/architecture/inference.py +160 -0
- codecortex/backends/__init__.py +35 -0
- codecortex/backends/base.py +38 -0
- codecortex/backends/context.py +118 -0
- codecortex/backends/contracts.py +65 -0
- codecortex/backends/factory.py +60 -0
- codecortex/backends/graph.py +107 -0
- codecortex/backends/manager.py +303 -0
- codecortex/backends/mcp_client.py +196 -0
- codecortex/backends/pool.py +128 -0
- codecortex/backends/spec.py +83 -0
- codecortex/backends/symbols.py +189 -0
- codecortex/benchmark.py +211 -0
- codecortex/cli.py +409 -0
- codecortex/config.py +27 -0
- codecortex/context/__init__.py +13 -0
- codecortex/context/budget.py +62 -0
- codecortex/context/integrated.py +60 -0
- codecortex/context/pipeline.py +223 -0
- codecortex/core/__init__.py +1 -0
- codecortex/core/contracts.py +45 -0
- codecortex/core/errors.py +17 -0
- codecortex/core/models.py +69 -0
- codecortex/dashboard.py +259 -0
- codecortex/editing.py +41 -0
- codecortex/engines/__init__.py +5 -0
- codecortex/engines/builtin/__init__.py +5 -0
- codecortex/engines/builtin/factory.py +26 -0
- codecortex/engines/builtin/memory.py +35 -0
- codecortex/engines/builtin/repository.py +73 -0
- codecortex/engines/builtin/symbols.py +89 -0
- codecortex/engines/builtin/validation.py +54 -0
- codecortex/engines/registry.py +26 -0
- codecortex/entrypoint.py +266 -0
- codecortex/evaluation/__init__.py +55 -0
- codecortex/evaluation/external.py +265 -0
- codecortex/evaluation/production.py +670 -0
- codecortex/evaluation/regression.py +188 -0
- codecortex/gateway.py +38 -0
- codecortex/git_intelligence.py +252 -0
- codecortex/indexing/__init__.py +6 -0
- codecortex/indexing/graph.py +78 -0
- codecortex/indexing/impact.py +127 -0
- codecortex/indexing/incremental.py +163 -0
- codecortex/indexing/incremental_graph.py +189 -0
- codecortex/indexing/indexer.py +172 -0
- codecortex/indexing/relationships.py +179 -0
- codecortex/indexing/resolution.py +88 -0
- codecortex/integrations/__init__.py +5 -0
- codecortex/integrations/agents.py +235 -0
- codecortex/interfaces/__init__.py +1 -0
- codecortex/interfaces/mcp_bridge.py +66 -0
- codecortex/languages/__init__.py +5 -0
- codecortex/languages/native.py +166 -0
- codecortex/languages/registry.py +232 -0
- codecortex/mcp/__init__.py +5 -0
- codecortex/mcp/extended.py +114 -0
- codecortex/mcp/server.py +473 -0
- codecortex/memory/__init__.py +11 -0
- codecortex/memory/json_store.py +52 -0
- codecortex/memory/knowledge.py +193 -0
- codecortex/memory/team_store.py +193 -0
- codecortex/orchestrator.py +153 -0
- codecortex/pr_intelligence.py +214 -0
- codecortex/retrieval/__init__.py +16 -0
- codecortex/retrieval/hybrid.py +67 -0
- codecortex/retrieval/index.py +135 -0
- codecortex/retrieval/providers.py +67 -0
- codecortex/retrieval/repository.py +94 -0
- codecortex/router/__init__.py +5 -0
- codecortex/router/router.py +79 -0
- codecortex/runtime.py +69 -0
- codecortex/setup.py +100 -0
- codecortex/symbols/__init__.py +5 -0
- codecortex/symbols/providers.py +192 -0
- codecortex/telemetry/__init__.py +5 -0
- codecortex/telemetry/collector.py +43 -0
- codecortex/tracing/__init__.py +9 -0
- codecortex/tracing/task_trace.py +235 -0
- codecortex/workspace/__init__.py +9 -0
- codecortex/workspace/federation.py +173 -0
- codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
- codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
- codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
- codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
- codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Warm process/session pool for long-lived isolated MCP backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from codecortex.backends.manager import BackendManager
|
|
13
|
+
from codecortex.backends.mcp_client import MCPStdioClient
|
|
14
|
+
from codecortex.backends.spec import BackendSpec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class _Session:
|
|
19
|
+
client: MCPStdioClient
|
|
20
|
+
lock: threading.RLock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BackendSessionPool:
|
|
24
|
+
"""Keep MCP backend processes warm and serialize traffic per process.
|
|
25
|
+
|
|
26
|
+
A failed request invalidates the process and retries once with a clean session. This
|
|
27
|
+
removes repeated process startup/initialize latency while keeping recovery bounded.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, manager: BackendManager) -> None:
|
|
31
|
+
self.manager = manager
|
|
32
|
+
self._sessions: dict[tuple[object, ...], _Session] = {}
|
|
33
|
+
self._lock = threading.RLock()
|
|
34
|
+
atexit.register(self.close_all)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _key(
|
|
38
|
+
spec: BackendSpec,
|
|
39
|
+
server_args: Sequence[str],
|
|
40
|
+
cwd: Path | None,
|
|
41
|
+
env: Mapping[str, str] | None,
|
|
42
|
+
) -> tuple[object, ...]:
|
|
43
|
+
return (
|
|
44
|
+
spec.key,
|
|
45
|
+
spec.revision,
|
|
46
|
+
tuple(server_args),
|
|
47
|
+
str(cwd.resolve()) if cwd else None,
|
|
48
|
+
tuple(sorted((env or {}).items())),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _session(
|
|
52
|
+
self,
|
|
53
|
+
spec: BackendSpec,
|
|
54
|
+
server_args: Sequence[str],
|
|
55
|
+
*,
|
|
56
|
+
cwd: Path | None = None,
|
|
57
|
+
env: Mapping[str, str] | None = None,
|
|
58
|
+
) -> tuple[tuple[object, ...], _Session]:
|
|
59
|
+
key = self._key(spec, server_args, cwd, env)
|
|
60
|
+
with self._lock:
|
|
61
|
+
session = self._sessions.get(key)
|
|
62
|
+
if session is None:
|
|
63
|
+
client = MCPStdioClient(
|
|
64
|
+
self.manager,
|
|
65
|
+
spec,
|
|
66
|
+
server_args,
|
|
67
|
+
cwd=cwd,
|
|
68
|
+
env=env,
|
|
69
|
+
)
|
|
70
|
+
client.start()
|
|
71
|
+
session = _Session(client=client, lock=threading.RLock())
|
|
72
|
+
self._sessions[key] = session
|
|
73
|
+
return key, session
|
|
74
|
+
|
|
75
|
+
def invalidate(self, key: tuple[object, ...]) -> None:
|
|
76
|
+
with self._lock:
|
|
77
|
+
session = self._sessions.pop(key, None)
|
|
78
|
+
if session is not None:
|
|
79
|
+
session.client.close()
|
|
80
|
+
|
|
81
|
+
def call_tool(
|
|
82
|
+
self,
|
|
83
|
+
spec: BackendSpec,
|
|
84
|
+
server_args: Sequence[str],
|
|
85
|
+
name: str,
|
|
86
|
+
arguments: Mapping[str, Any] | None = None,
|
|
87
|
+
*,
|
|
88
|
+
cwd: Path | None = None,
|
|
89
|
+
env: Mapping[str, str] | None = None,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
last: Exception | None = None
|
|
92
|
+
for attempt in range(2):
|
|
93
|
+
key, session = self._session(spec, server_args, cwd=cwd, env=env)
|
|
94
|
+
try:
|
|
95
|
+
with session.lock:
|
|
96
|
+
return session.client.call_tool(name, arguments)
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
last = exc
|
|
99
|
+
self.invalidate(key)
|
|
100
|
+
if attempt:
|
|
101
|
+
raise
|
|
102
|
+
assert last is not None
|
|
103
|
+
raise last
|
|
104
|
+
|
|
105
|
+
def tools(
|
|
106
|
+
self,
|
|
107
|
+
spec: BackendSpec,
|
|
108
|
+
server_args: Sequence[str],
|
|
109
|
+
*,
|
|
110
|
+
cwd: Path | None = None,
|
|
111
|
+
env: Mapping[str, str] | None = None,
|
|
112
|
+
) -> list[dict[str, Any]]:
|
|
113
|
+
key, session = self._session(spec, server_args, cwd=cwd, env=env)
|
|
114
|
+
try:
|
|
115
|
+
with session.lock:
|
|
116
|
+
return session.client.tools()
|
|
117
|
+
except Exception:
|
|
118
|
+
self.invalidate(key)
|
|
119
|
+
key, session = self._session(spec, server_args, cwd=cwd, env=env)
|
|
120
|
+
with session.lock:
|
|
121
|
+
return session.client.tools()
|
|
122
|
+
|
|
123
|
+
def close_all(self) -> None:
|
|
124
|
+
with self._lock:
|
|
125
|
+
sessions = list(self._sessions.values())
|
|
126
|
+
self._sessions.clear()
|
|
127
|
+
for session in sessions:
|
|
128
|
+
session.client.close()
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Configuration-driven optional backend specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class BackendSpec:
|
|
11
|
+
key: str
|
|
12
|
+
capabilities: tuple[str, ...]
|
|
13
|
+
package: str = ""
|
|
14
|
+
source_url: str = ""
|
|
15
|
+
revision: str = ""
|
|
16
|
+
command: str = ""
|
|
17
|
+
license_id: str = ""
|
|
18
|
+
extras: tuple[str, ...] = ()
|
|
19
|
+
python: str = "3.13"
|
|
20
|
+
vendor_path: str | None = None
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def configured(self) -> bool:
|
|
24
|
+
return bool(
|
|
25
|
+
self.package
|
|
26
|
+
and self.source_url
|
|
27
|
+
and len(self.revision) == 40
|
|
28
|
+
and self.command
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def source_requirement(self) -> str:
|
|
33
|
+
if not self.configured:
|
|
34
|
+
raise RuntimeError(f"backend {self.key!r} is not configured")
|
|
35
|
+
if not self.extras:
|
|
36
|
+
return f"git+{self.source_url}@{self.revision}"
|
|
37
|
+
extras = ",".join(self.extras)
|
|
38
|
+
return f"{self.package}[{extras}] @ git+{self.source_url}@{self.revision}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _external_spec(
|
|
42
|
+
key: str,
|
|
43
|
+
capabilities: tuple[str, ...],
|
|
44
|
+
*,
|
|
45
|
+
default_extras: tuple[str, ...] = (),
|
|
46
|
+
) -> BackendSpec:
|
|
47
|
+
prefix = f"CODECORTEX_{key.upper()}_BACKEND"
|
|
48
|
+
extras_value = os.getenv(f"{prefix}_EXTRAS", "").strip()
|
|
49
|
+
extras = (
|
|
50
|
+
tuple(part.strip() for part in extras_value.split(",") if part.strip())
|
|
51
|
+
if extras_value
|
|
52
|
+
else default_extras
|
|
53
|
+
)
|
|
54
|
+
vendor_path = os.getenv(f"{prefix}_LOCAL_PATH", "").strip() or None
|
|
55
|
+
return BackendSpec(
|
|
56
|
+
key=key,
|
|
57
|
+
capabilities=capabilities,
|
|
58
|
+
package=os.getenv(f"{prefix}_PACKAGE", "").strip(),
|
|
59
|
+
source_url=os.getenv(f"{prefix}_SOURCE_URL", "").strip(),
|
|
60
|
+
revision=os.getenv(f"{prefix}_REVISION", "").strip(),
|
|
61
|
+
command=os.getenv(f"{prefix}_COMMAND", "").strip(),
|
|
62
|
+
license_id=os.getenv(f"{prefix}_LICENSE", "").strip(),
|
|
63
|
+
extras=extras,
|
|
64
|
+
python=os.getenv(f"{prefix}_PYTHON", "3.13").strip() or "3.13",
|
|
65
|
+
vendor_path=vendor_path,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
BACKENDS: dict[str, BackendSpec] = {
|
|
70
|
+
"graph": _external_spec(
|
|
71
|
+
"graph",
|
|
72
|
+
("ast", "graph", "query", "path", "explain", "incremental"),
|
|
73
|
+
),
|
|
74
|
+
"symbols": _external_spec(
|
|
75
|
+
"symbols",
|
|
76
|
+
("lsp", "symbols", "references", "diagnostics", "editing", "refactor"),
|
|
77
|
+
),
|
|
78
|
+
"context": _external_spec(
|
|
79
|
+
"context",
|
|
80
|
+
("compression", "routing", "reversible", "memory", "proxy", "mcp"),
|
|
81
|
+
default_extras=("mcp", "code", "memory", "relevance"),
|
|
82
|
+
),
|
|
83
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""IDE-grade semantic symbol backend adapter with guarded edits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from codecortex.backends.base import ManagedAdapterMixin
|
|
12
|
+
from codecortex.backends.manager import BackendManager
|
|
13
|
+
from codecortex.backends.mcp_client import MCPStdioClient
|
|
14
|
+
from codecortex.backends.pool import BackendSessionPool
|
|
15
|
+
from codecortex.backends.spec import BACKENDS
|
|
16
|
+
from codecortex.core.contracts import Engine
|
|
17
|
+
from codecortex.core.models import AgentRequest, Capability, ContextChunk, EngineResult, RequestKind
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SymbolBackendAdapter(ManagedAdapterMixin, Engine):
|
|
21
|
+
capability = Capability.SYMBOLS
|
|
22
|
+
required_tools = {"find_symbol", "find_referencing_symbols"}
|
|
23
|
+
editing_tools = {
|
|
24
|
+
"rename_symbol",
|
|
25
|
+
"replace_symbol_body",
|
|
26
|
+
"insert_before_symbol",
|
|
27
|
+
"insert_after_symbol",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def __init__(self, project_root: Path, manager: BackendManager | None = None) -> None:
|
|
31
|
+
self.project_root = project_root.resolve()
|
|
32
|
+
self.manager = manager or BackendManager()
|
|
33
|
+
self.spec = BACKENDS["symbols"]
|
|
34
|
+
self.pool = BackendSessionPool(self.manager)
|
|
35
|
+
|
|
36
|
+
async def health(self) -> bool:
|
|
37
|
+
return await asyncio.to_thread(self.manager.probe, self.spec, False)
|
|
38
|
+
|
|
39
|
+
def server_args(self) -> tuple[str, ...]:
|
|
40
|
+
return (
|
|
41
|
+
"start-mcp-server",
|
|
42
|
+
"--transport",
|
|
43
|
+
"stdio",
|
|
44
|
+
"--project",
|
|
45
|
+
str(self.project_root),
|
|
46
|
+
"--enable-web-dashboard",
|
|
47
|
+
"false",
|
|
48
|
+
"--open-web-dashboard",
|
|
49
|
+
"false",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def tools(self) -> list[dict[str, Any]]:
|
|
53
|
+
tools = self.pool.tools(
|
|
54
|
+
self.spec,
|
|
55
|
+
self.server_args(),
|
|
56
|
+
cwd=self.project_root,
|
|
57
|
+
)
|
|
58
|
+
self.require_tools(tools, self.required_tools)
|
|
59
|
+
return tools
|
|
60
|
+
|
|
61
|
+
def call(self, tool: str, arguments: Mapping[str, Any]) -> dict[str, Any]:
|
|
62
|
+
return self.pool.call_tool(
|
|
63
|
+
self.spec,
|
|
64
|
+
self.server_args(),
|
|
65
|
+
tool,
|
|
66
|
+
arguments,
|
|
67
|
+
cwd=self.project_root,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _relative_path(self, value: str) -> str:
|
|
71
|
+
candidate = (self.project_root / value).resolve()
|
|
72
|
+
try:
|
|
73
|
+
relative = candidate.relative_to(self.project_root)
|
|
74
|
+
except ValueError as exc:
|
|
75
|
+
raise ValueError("symbol edits must stay inside the project root") from exc
|
|
76
|
+
if not candidate.exists():
|
|
77
|
+
raise ValueError(f"path does not exist: {value}")
|
|
78
|
+
if not candidate.is_file():
|
|
79
|
+
raise ValueError(f"expected a file path: {value}")
|
|
80
|
+
return relative.as_posix()
|
|
81
|
+
|
|
82
|
+
def _require_edit_tool(self, name: str) -> None:
|
|
83
|
+
available = {str(item.get("name")) for item in self.tools()}
|
|
84
|
+
if name not in available:
|
|
85
|
+
raise RuntimeError(f"symbol backend does not expose required edit tool: {name}")
|
|
86
|
+
|
|
87
|
+
def preflight_symbol(self, name_path: str, relative_path: str) -> dict[str, Any]:
|
|
88
|
+
relative_path = self._relative_path(relative_path)
|
|
89
|
+
return self.call(
|
|
90
|
+
"find_symbol",
|
|
91
|
+
{
|
|
92
|
+
"name_path_pattern": name_path,
|
|
93
|
+
"relative_path": relative_path,
|
|
94
|
+
"include_body": True,
|
|
95
|
+
"max_matches": 2,
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def rename_symbol(self, name_path: str, relative_path: str, new_name: str) -> dict[str, Any]:
|
|
100
|
+
relative_path = self._relative_path(relative_path)
|
|
101
|
+
if not new_name.strip():
|
|
102
|
+
raise ValueError("new_name cannot be empty")
|
|
103
|
+
self._require_edit_tool("rename_symbol")
|
|
104
|
+
self.preflight_symbol(name_path, relative_path)
|
|
105
|
+
return self.call(
|
|
106
|
+
"rename_symbol",
|
|
107
|
+
{"name_path": name_path, "relative_path": relative_path, "new_name": new_name},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def replace_symbol_body(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
|
|
111
|
+
relative_path = self._relative_path(relative_path)
|
|
112
|
+
if not body.strip():
|
|
113
|
+
raise ValueError("replacement body cannot be empty")
|
|
114
|
+
self._require_edit_tool("replace_symbol_body")
|
|
115
|
+
self.preflight_symbol(name_path, relative_path)
|
|
116
|
+
return self.call(
|
|
117
|
+
"replace_symbol_body",
|
|
118
|
+
{"name_path": name_path, "relative_path": relative_path, "body": body},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def insert_before_symbol(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
|
|
122
|
+
relative_path = self._relative_path(relative_path)
|
|
123
|
+
self._require_edit_tool("insert_before_symbol")
|
|
124
|
+
self.preflight_symbol(name_path, relative_path)
|
|
125
|
+
return self.call(
|
|
126
|
+
"insert_before_symbol",
|
|
127
|
+
{"name_path": name_path, "relative_path": relative_path, "body": body},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def insert_after_symbol(self, name_path: str, relative_path: str, body: str) -> dict[str, Any]:
|
|
131
|
+
relative_path = self._relative_path(relative_path)
|
|
132
|
+
self._require_edit_tool("insert_after_symbol")
|
|
133
|
+
self.preflight_symbol(name_path, relative_path)
|
|
134
|
+
return self.call(
|
|
135
|
+
"insert_after_symbol",
|
|
136
|
+
{"name_path": name_path, "relative_path": relative_path, "body": body},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def execute(self, request: AgentRequest) -> EngineResult:
|
|
140
|
+
return await asyncio.to_thread(self._execute_sync, request)
|
|
141
|
+
|
|
142
|
+
def _execute_sync(self, request: AgentRequest) -> EngineResult:
|
|
143
|
+
explicit_tool = request.metadata.get("symbol_tool")
|
|
144
|
+
explicit_args = request.metadata.get("symbol_arguments")
|
|
145
|
+
if isinstance(explicit_tool, str):
|
|
146
|
+
arguments = dict(explicit_args) if isinstance(explicit_args, Mapping) else {}
|
|
147
|
+
result = self.call(explicit_tool, arguments)
|
|
148
|
+
tool = explicit_tool
|
|
149
|
+
else:
|
|
150
|
+
tool, arguments = self._plan(request)
|
|
151
|
+
result = self.call(tool, arguments)
|
|
152
|
+
content = MCPStdioClient.content_text(result) or json.dumps(result, ensure_ascii=False)
|
|
153
|
+
return EngineResult(
|
|
154
|
+
capability=self.capability,
|
|
155
|
+
content=content,
|
|
156
|
+
chunks=[
|
|
157
|
+
ContextChunk(
|
|
158
|
+
source=f"symbol:{tool}",
|
|
159
|
+
content=content,
|
|
160
|
+
tokens=max(1, len(content) // 4),
|
|
161
|
+
relevance=0.98,
|
|
162
|
+
metadata={"backend": self.spec.key, "tool": tool},
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
if content
|
|
166
|
+
else [],
|
|
167
|
+
metadata={"backend": self.spec.key, "revision": self.spec.revision, "tool": tool},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _plan(request: AgentRequest) -> tuple[str, dict[str, Any]]:
|
|
172
|
+
relative_path = request.metadata.get("relative_path")
|
|
173
|
+
if request.kind in {RequestKind.REFACTOR, RequestKind.CHANGE}:
|
|
174
|
+
return "find_symbol", {
|
|
175
|
+
"name_path_pattern": request.query,
|
|
176
|
+
"include_body": True,
|
|
177
|
+
**({"relative_path": relative_path} if isinstance(relative_path, str) else {}),
|
|
178
|
+
}
|
|
179
|
+
if request.metadata.get("references") and isinstance(relative_path, str):
|
|
180
|
+
return "find_referencing_symbols", {
|
|
181
|
+
"name_path": request.query,
|
|
182
|
+
"relative_path": relative_path,
|
|
183
|
+
}
|
|
184
|
+
return "find_symbol", {
|
|
185
|
+
"name_path_pattern": request.query,
|
|
186
|
+
"include_body": request.kind in {RequestKind.DEBUG, RequestKind.REVIEW},
|
|
187
|
+
"depth": 1,
|
|
188
|
+
**({"relative_path": relative_path} if isinstance(relative_path, str) else {}),
|
|
189
|
+
}
|
codecortex/benchmark.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Reproducible repository intelligence benchmark harness."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import perf_counter
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
from codecortex.indexing.indexer import ProjectIndexer
|
|
12
|
+
|
|
13
|
+
_TEXT_SUFFIXES = {
|
|
14
|
+
".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".rs", ".java",
|
|
15
|
+
".c", ".h", ".cc", ".cpp", ".hpp", ".cs", ".php", ".rb",
|
|
16
|
+
".md", ".toml", ".yaml", ".yml", ".json",
|
|
17
|
+
}
|
|
18
|
+
_EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class BenchmarkCase:
|
|
23
|
+
id: str
|
|
24
|
+
query: str
|
|
25
|
+
expected_paths: tuple[str, ...] = ()
|
|
26
|
+
expected_symbols: tuple[str, ...] = ()
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, value: dict[str, object]) -> BenchmarkCase:
|
|
30
|
+
return cls(
|
|
31
|
+
id=str(value["id"]),
|
|
32
|
+
query=str(value["query"]),
|
|
33
|
+
expected_paths=tuple(str(item) for item in value.get("expected_paths", [])),
|
|
34
|
+
expected_symbols=tuple(str(item) for item in value.get("expected_symbols", [])),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class StrategyResult:
|
|
40
|
+
strategy: str
|
|
41
|
+
case_id: str
|
|
42
|
+
duration_ms: float
|
|
43
|
+
context_tokens: int
|
|
44
|
+
files_read: int
|
|
45
|
+
tool_calls: int
|
|
46
|
+
path_recall: float
|
|
47
|
+
symbol_recall: float
|
|
48
|
+
success: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class BenchmarkReport:
|
|
53
|
+
results: tuple[StrategyResult, ...]
|
|
54
|
+
|
|
55
|
+
def summary(self) -> dict[str, dict[str, float]]:
|
|
56
|
+
grouped: dict[str, list[StrategyResult]] = {}
|
|
57
|
+
for result in self.results:
|
|
58
|
+
grouped.setdefault(result.strategy, []).append(result)
|
|
59
|
+
summary: dict[str, dict[str, float]] = {}
|
|
60
|
+
for name, rows in grouped.items():
|
|
61
|
+
count = max(1, len(rows))
|
|
62
|
+
summary[name] = {
|
|
63
|
+
"cases": float(len(rows)),
|
|
64
|
+
"success_rate": sum(row.success for row in rows) / count,
|
|
65
|
+
"avg_duration_ms": sum(row.duration_ms for row in rows) / count,
|
|
66
|
+
"avg_context_tokens": sum(row.context_tokens for row in rows) / count,
|
|
67
|
+
"avg_files_read": sum(row.files_read for row in rows) / count,
|
|
68
|
+
"avg_tool_calls": sum(row.tool_calls for row in rows) / count,
|
|
69
|
+
"avg_path_recall": sum(row.path_recall for row in rows) / count,
|
|
70
|
+
"avg_symbol_recall": sum(row.symbol_recall for row in rows) / count,
|
|
71
|
+
}
|
|
72
|
+
return summary
|
|
73
|
+
|
|
74
|
+
def save(self, output: Path) -> None:
|
|
75
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
payload = {
|
|
77
|
+
"summary": self.summary(),
|
|
78
|
+
"results": [asdict(result) for result in self.results],
|
|
79
|
+
}
|
|
80
|
+
output.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BenchmarkStrategy(Protocol):
|
|
84
|
+
name: str
|
|
85
|
+
|
|
86
|
+
def run(self, case: BenchmarkCase) -> StrategyResult: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _recall(expected: tuple[str, ...], actual: set[str]) -> float:
|
|
90
|
+
if not expected:
|
|
91
|
+
return 1.0
|
|
92
|
+
normalized = {item.lower() for item in actual}
|
|
93
|
+
hits = sum(
|
|
94
|
+
1
|
|
95
|
+
for item in expected
|
|
96
|
+
if item.lower() in normalized
|
|
97
|
+
or any(item.lower() in candidate for candidate in normalized)
|
|
98
|
+
)
|
|
99
|
+
return hits / len(expected)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _success(case: BenchmarkCase, path_recall: float, symbol_recall: float) -> bool:
|
|
103
|
+
paths_ok = not case.expected_paths or path_recall > 0
|
|
104
|
+
symbols_ok = not case.expected_symbols or symbol_recall > 0
|
|
105
|
+
return paths_ok and symbols_ok
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class FullTextBaseline:
|
|
109
|
+
"""Simple full-repository lexical baseline used for measured comparisons."""
|
|
110
|
+
|
|
111
|
+
name = "full_text_baseline"
|
|
112
|
+
|
|
113
|
+
def __init__(self, root: Path, max_files: int = 10_000, result_limit: int = 50) -> None:
|
|
114
|
+
self.root = root.resolve()
|
|
115
|
+
self.max_files = max_files
|
|
116
|
+
self.result_limit = result_limit
|
|
117
|
+
|
|
118
|
+
def run(self, case: BenchmarkCase) -> StrategyResult:
|
|
119
|
+
started = perf_counter()
|
|
120
|
+
terms = {term.lower() for term in case.query.split() if len(term) > 2}
|
|
121
|
+
scored: list[tuple[int, str, str]] = []
|
|
122
|
+
files_read = 0
|
|
123
|
+
for path in self.root.rglob("*"):
|
|
124
|
+
if files_read >= self.max_files:
|
|
125
|
+
break
|
|
126
|
+
if not path.is_file() or path.suffix.lower() not in _TEXT_SUFFIXES:
|
|
127
|
+
continue
|
|
128
|
+
relative = path.relative_to(self.root)
|
|
129
|
+
if any(part in _EXCLUDED for part in relative.parts):
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
text = path.read_text(encoding="utf-8")
|
|
133
|
+
except (OSError, UnicodeDecodeError):
|
|
134
|
+
continue
|
|
135
|
+
files_read += 1
|
|
136
|
+
lowered = text.lower()
|
|
137
|
+
path_text = relative.as_posix().lower()
|
|
138
|
+
score = sum(lowered.count(term) + (3 if term in path_text else 0) for term in terms)
|
|
139
|
+
if score:
|
|
140
|
+
scored.append((score, relative.as_posix(), text[:4000]))
|
|
141
|
+
scored.sort(key=lambda item: (-item[0], item[1]))
|
|
142
|
+
selected = scored[: self.result_limit]
|
|
143
|
+
paths = {path for _, path, _ in selected}
|
|
144
|
+
symbols: set[str] = set()
|
|
145
|
+
context = "\n".join(f"[{path}]\n{text}" for _, path, text in selected)
|
|
146
|
+
path_recall = _recall(case.expected_paths, paths)
|
|
147
|
+
symbol_recall = _recall(case.expected_symbols, symbols)
|
|
148
|
+
return StrategyResult(
|
|
149
|
+
strategy=self.name,
|
|
150
|
+
case_id=case.id,
|
|
151
|
+
duration_ms=(perf_counter() - started) * 1000,
|
|
152
|
+
context_tokens=max(0, len(context) // 4),
|
|
153
|
+
files_read=files_read,
|
|
154
|
+
tool_calls=1,
|
|
155
|
+
path_recall=path_recall,
|
|
156
|
+
symbol_recall=symbol_recall,
|
|
157
|
+
success=_success(case, path_recall, symbol_recall),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CodeCortexGraphStrategy:
|
|
162
|
+
name = "codecortex_graph"
|
|
163
|
+
|
|
164
|
+
def __init__(self, root: Path, result_limit: int = 50) -> None:
|
|
165
|
+
self.root = root.resolve()
|
|
166
|
+
self.result_limit = result_limit
|
|
167
|
+
self.graph = ProjectIndexer(self.root).build()
|
|
168
|
+
|
|
169
|
+
def run(self, case: BenchmarkCase) -> StrategyResult:
|
|
170
|
+
started = perf_counter()
|
|
171
|
+
matches = self.graph.search(case.query, self.result_limit)
|
|
172
|
+
paths = {node.path for node in matches if node.path}
|
|
173
|
+
symbols = {node.name for node in matches if node.kind not in {"file", "module", "reference"}}
|
|
174
|
+
lines = [
|
|
175
|
+
f"{node.kind} {node.name} {node.path or ''}:{node.line or ''}"
|
|
176
|
+
for node in matches
|
|
177
|
+
]
|
|
178
|
+
context = "\n".join(lines)
|
|
179
|
+
path_recall = _recall(case.expected_paths, {path for path in paths if path})
|
|
180
|
+
symbol_recall = _recall(case.expected_symbols, symbols)
|
|
181
|
+
return StrategyResult(
|
|
182
|
+
strategy=self.name,
|
|
183
|
+
case_id=case.id,
|
|
184
|
+
duration_ms=(perf_counter() - started) * 1000,
|
|
185
|
+
context_tokens=max(0, len(context) // 4),
|
|
186
|
+
files_read=len({path for path in paths if path}),
|
|
187
|
+
tool_calls=1,
|
|
188
|
+
path_recall=path_recall,
|
|
189
|
+
symbol_recall=symbol_recall,
|
|
190
|
+
success=_success(case, path_recall, symbol_recall),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class BenchmarkSuite:
|
|
195
|
+
def __init__(self, cases: list[BenchmarkCase], strategies: list[BenchmarkStrategy]) -> None:
|
|
196
|
+
self.cases = cases
|
|
197
|
+
self.strategies = strategies
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def load(cls, path: Path, strategies: list[BenchmarkStrategy]) -> BenchmarkSuite:
|
|
201
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
202
|
+
cases = [BenchmarkCase.from_dict(item) for item in payload["cases"]]
|
|
203
|
+
return cls(cases, strategies)
|
|
204
|
+
|
|
205
|
+
def run(self) -> BenchmarkReport:
|
|
206
|
+
results = tuple(
|
|
207
|
+
strategy.run(case)
|
|
208
|
+
for case in self.cases
|
|
209
|
+
for strategy in self.strategies
|
|
210
|
+
)
|
|
211
|
+
return BenchmarkReport(results)
|