codrspot-processor-mcp 0.1.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.
- codepreproc_client/__init__.py +14 -0
- codepreproc_client/__main__.py +9 -0
- codepreproc_client/layer1_business/__init__.py +0 -0
- codepreproc_client/layer1_business/allowed_scope_cache.py +62 -0
- codepreproc_client/layer1_business/api_client/__init__.py +0 -0
- codepreproc_client/layer1_business/api_client/lease_client.py +34 -0
- codepreproc_client/layer1_business/api_client/net_guard.py +98 -0
- codepreproc_client/layer1_business/api_client/promote_client.py +59 -0
- codepreproc_client/layer1_business/api_client/reasoning_client.py +163 -0
- codepreproc_client/layer1_business/api_client/registry_client.py +119 -0
- codepreproc_client/layer1_business/api_client/superkg_client.py +327 -0
- codepreproc_client/layer1_business/cli/__init__.py +0 -0
- codepreproc_client/layer1_business/cli/init.py +578 -0
- codepreproc_client/layer1_business/cli/main.py +78 -0
- codepreproc_client/layer1_business/config.py +104 -0
- codepreproc_client/layer1_business/git_utils.py +67 -0
- codepreproc_client/layer1_business/license/__init__.py +0 -0
- codepreproc_client/layer1_business/license/fingerprint.py +61 -0
- codepreproc_client/layer1_business/license/jwt_client.py +60 -0
- codepreproc_client/layer1_business/license/project_lock.py +44 -0
- codepreproc_client/layer1_business/local_registry.py +40 -0
- codepreproc_client/layer1_business/manifest/__init__.py +0 -0
- codepreproc_client/layer1_business/manifest/credentials.py +35 -0
- codepreproc_client/layer1_business/mcp/__init__.py +0 -0
- codepreproc_client/layer1_business/mcp/action_monitor.py +246 -0
- codepreproc_client/layer1_business/mcp/lifecycle.py +649 -0
- codepreproc_client/layer1_business/mcp/server.py +656 -0
- codepreproc_client/layer1_business/mcp/tools.py +1665 -0
- codepreproc_client/layer1_business/project_registry.py +99 -0
- codepreproc_client/layer2_tooling/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/ast_graph.py +426 -0
- codepreproc_client/layer2_tooling/condensation/bm25_store.py +132 -0
- codepreproc_client/layer2_tooling/condensation/chunking/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/chunking/ast_chunker.py +313 -0
- codepreproc_client/layer2_tooling/condensation/chunking/languages.py +238 -0
- codepreproc_client/layer2_tooling/condensation/embed_worker.py +128 -0
- codepreproc_client/layer2_tooling/condensation/embeddings.py +452 -0
- codepreproc_client/layer2_tooling/condensation/extract/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/extract/compactor.py +103 -0
- codepreproc_client/layer2_tooling/condensation/extract/providers/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/extract/providers/base.py +38 -0
- codepreproc_client/layer2_tooling/condensation/indexer.py +945 -0
- codepreproc_client/layer2_tooling/condensation/ppl/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/ppl/emitter.py +162 -0
- codepreproc_client/layer2_tooling/condensation/ppl/parser.py +352 -0
- codepreproc_client/layer2_tooling/condensation/qdrant_store.py +387 -0
- codepreproc_client/layer2_tooling/condensation/scanning/__init__.py +0 -0
- codepreproc_client/layer2_tooling/condensation/scanning/manifest_scanner.py +72 -0
- codepreproc_client/layer2_tooling/condensation/scanning/md_scanner.py +97 -0
- codepreproc_client/layer2_tooling/condensation/scanning/repo_mapper.py +495 -0
- codepreproc_client/layer2_tooling/condensation/session_vector_store.py +69 -0
- codepreproc_client/layer2_tooling/coupling/__init__.py +0 -0
- codepreproc_client/layer2_tooling/coupling/domain.py +263 -0
- codepreproc_client/layer2_tooling/coupling/graph.py +255 -0
- codepreproc_client/layer2_tooling/coupling/layer2_gate.py +189 -0
- codepreproc_client/layer2_tooling/coupling/projector.py +139 -0
- codepreproc_client/layer2_tooling/coupling/resolver.py +150 -0
- codepreproc_client/layer2_tooling/materialization/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/execute_semantic.py +1249 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/filesystem_reorg.py +575 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/patch_generator.py +127 -0
- codepreproc_client/layer2_tooling/materialization/pipeline/patch_validator.py +101 -0
- codepreproc_client/layer2_tooling/materialization/semantic/__init__.py +19 -0
- codepreproc_client/layer2_tooling/materialization/semantic/anchors.py +61 -0
- codepreproc_client/layer2_tooling/materialization/semantic/applicability.py +75 -0
- codepreproc_client/layer2_tooling/materialization/semantic/edit_planner.py +264 -0
- codepreproc_client/layer2_tooling/materialization/semantic/materializer.py +121 -0
- codepreproc_client/layer2_tooling/materialization/semantic/merger.py +73 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/add_import.py +36 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/append_argument.py +37 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/insert_literal.py +24 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_import.py +33 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/remove_statement_unique.py +14 -0
- codepreproc_client/layer2_tooling/materialization/semantic/ops/rename_symbol_local.py +16 -0
- codepreproc_client/layer2_tooling/materialization/semantic/region_resolver.py +229 -0
- codepreproc_client/layer2_tooling/materialization/semantic/synthesizer.py +186 -0
- codepreproc_client/layer2_tooling/materialization/semantic/target_locator.py +94 -0
- codepreproc_client/layer2_tooling/materialization/semantic/validator.py +322 -0
- codepreproc_client/layer2_tooling/materialization/snippet/__init__.py +0 -0
- codepreproc_client/layer2_tooling/materialization/snippet/assembler.py +503 -0
- codepreproc_client/layer2_tooling/materialization/snippet/loader.py +187 -0
- codepreproc_client/layer2_tooling/retrieval/__init__.py +0 -0
- codepreproc_client/layer2_tooling/retrieval/graph_walk.py +47 -0
- codepreproc_client/layer2_tooling/retrieval/hybrid.py +95 -0
- codepreproc_client/layer2_tooling/retrieval/rerank_worker.py +86 -0
- codepreproc_client/layer2_tooling/retrieval/reranker.py +213 -0
- codepreproc_client/layer2_tooling/watcher/__init__.py +0 -0
- codepreproc_client/layer2_tooling/watcher/fs_watcher.py +3 -0
- codrspot_processor_mcp-0.1.0.dist-info/METADATA +396 -0
- codrspot_processor_mcp-0.1.0.dist-info/RECORD +100 -0
- codrspot_processor_mcp-0.1.0.dist-info/WHEEL +5 -0
- codrspot_processor_mcp-0.1.0.dist-info/entry_points.txt +3 -0
- codrspot_processor_mcp-0.1.0.dist-info/licenses/LICENSE +23 -0
- codrspot_processor_mcp-0.1.0.dist-info/top_level.txt +2 -0
- contracts/__init__.py +0 -0
- contracts/dtos.py +1239 -0
- contracts/ir.py +102 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""codepreproc package."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
# GitPython raises ImportError at import time if it can't find a `git`
|
|
6
|
+
# executable on PATH, which would take down every module that does
|
|
7
|
+
# `from git import Repo` (tools, indexer, registry, ...) even when the
|
|
8
|
+
# caller never touches git. Downgrade that to a warning so git-dependent
|
|
9
|
+
# features fail only when actually used.
|
|
10
|
+
os.environ.setdefault("GIT_PYTHON_REFRESH", "warn")
|
|
11
|
+
|
|
12
|
+
__all__ = ["__version__"]
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Local, on-disk cache of the projects the caller's tenant/API key is allowed
|
|
2
|
+
to see (registry_client.list_projects, backed by GET /v1/projects).
|
|
3
|
+
|
|
4
|
+
Mirrors local_registry.py: the server is the source of truth, but the
|
|
5
|
+
client shouldn't need a network round trip just to know which project_ids
|
|
6
|
+
it's allowed to use. Refreshed once after mint if the cache is empty, and
|
|
7
|
+
whenever the server rejects a project_id as unauthorized (the cache may be
|
|
8
|
+
stale - a project was added to or removed from the tenant since).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from contracts.dtos import ProjectConfig
|
|
17
|
+
|
|
18
|
+
_FILENAME = "allowed_projects.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cache_path(home_dir: Path) -> Path:
|
|
22
|
+
return home_dir / _FILENAME
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_allowed_projects(home_dir: Path) -> list[ProjectConfig]:
|
|
26
|
+
path = _cache_path(home_dir)
|
|
27
|
+
if not path.exists():
|
|
28
|
+
return []
|
|
29
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
30
|
+
return [ProjectConfig.model_validate(data) for data in raw]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_allowed_projects(home_dir: Path, projects: list[ProjectConfig]) -> None:
|
|
34
|
+
path = _cache_path(home_dir)
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
path.write_text(
|
|
37
|
+
json.dumps([p.model_dump(mode="json") for p in projects], indent=2),
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def clear_allowed_projects(home_dir: Path) -> None:
|
|
43
|
+
path = _cache_path(home_dir)
|
|
44
|
+
if path.exists():
|
|
45
|
+
path.unlink()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def get_allowed_projects(
|
|
49
|
+
*, home_dir: Path, base_url: str, jwt: str, force_refresh: bool = False
|
|
50
|
+
) -> list[ProjectConfig]:
|
|
51
|
+
"""Return the cached allowed-project list, fetching and caching it first if needed."""
|
|
52
|
+
|
|
53
|
+
if not force_refresh:
|
|
54
|
+
cached = load_allowed_projects(home_dir)
|
|
55
|
+
if cached:
|
|
56
|
+
return cached
|
|
57
|
+
|
|
58
|
+
from codepreproc_client.layer1_business.api_client.registry_client import list_projects
|
|
59
|
+
|
|
60
|
+
projects = await list_projects(base_url=base_url, jwt=jwt)
|
|
61
|
+
save_allowed_projects(home_dir, projects)
|
|
62
|
+
return projects
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Client for POST /v1/lease_world_model (contracts/openapi.yaml).
|
|
2
|
+
|
|
3
|
+
Hands out a temporary, session-scoped copy of the licensed-scope world
|
|
4
|
+
model so the client can plan/apply edits locally without a round-trip per
|
|
5
|
+
operation (architecture doc S5). The returned lease MUST be discarded on
|
|
6
|
+
the client's last MCP call - see mcp/lifecycle.py, which owns teardown.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from contracts.dtos import LensContract, WorldModelLease
|
|
14
|
+
|
|
15
|
+
DEFAULT_TIMEOUT_S = 30.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def lease_world_model(
|
|
19
|
+
*,
|
|
20
|
+
base_url: str,
|
|
21
|
+
jwt: str,
|
|
22
|
+
project_scope: LensContract,
|
|
23
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
24
|
+
) -> WorldModelLease:
|
|
25
|
+
"""Request a temporary world-model copy scoped to `project_scope`."""
|
|
26
|
+
|
|
27
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
28
|
+
response = await client.post(
|
|
29
|
+
"/v1/lease_world_model",
|
|
30
|
+
json={"project_scope": project_scope.model_dump(mode="json")},
|
|
31
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
32
|
+
)
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
return WorldModelLease.model_validate(response.json())
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Outbound payload invariant: no raw source, no source-derived vectors.
|
|
2
|
+
|
|
3
|
+
Architecture doc S3/S12: every call through ``api_client`` must cross the
|
|
4
|
+
network boundary carrying only condensate (``Gist``/``EdgeRef``/etc.), never
|
|
5
|
+
raw source text or embeddings computed over raw source. This module is the
|
|
6
|
+
single chokepoint that asserts that invariant before a payload is sent -
|
|
7
|
+
CI-tested, not trusted to code review (S3).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
# DTOs that are allowed to cross the network boundary (contracts/openapi.yaml).
|
|
17
|
+
# Anything else nested inside an outbound payload is rejected.
|
|
18
|
+
_ALLOWED_DTO_NAMES = {
|
|
19
|
+
"Manifest",
|
|
20
|
+
"FingerprintAttestation",
|
|
21
|
+
"MintRequest",
|
|
22
|
+
"MintResponse",
|
|
23
|
+
"Gist",
|
|
24
|
+
"EdgeRef",
|
|
25
|
+
"Completeness",
|
|
26
|
+
"LensContract",
|
|
27
|
+
"PromoteRequest",
|
|
28
|
+
"StampedResult",
|
|
29
|
+
"LeaseWorldModelRequest",
|
|
30
|
+
"WorldModelLease",
|
|
31
|
+
"ResolveCouplingRequest",
|
|
32
|
+
"CouplingReport",
|
|
33
|
+
"RegisterProjectRequest",
|
|
34
|
+
"SetSuperkgIdRequest",
|
|
35
|
+
"ErrorResponse",
|
|
36
|
+
"SuperkgBootstrapRequest",
|
|
37
|
+
"SuperkgSessionOpenRequest",
|
|
38
|
+
"SuperkgSessionCloseRequest",
|
|
39
|
+
"SuperkgDecisionRequest",
|
|
40
|
+
"SuperkgLogActivityRequest",
|
|
41
|
+
"SuperkgTransitionRequest",
|
|
42
|
+
"SuperkgQueryRequest",
|
|
43
|
+
"SuperkgChunkMetadata",
|
|
44
|
+
"SuperkgIngestRequest",
|
|
45
|
+
"SuperkgEnrichRequest",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Field names that would indicate raw source or source-derived vectors leaking
|
|
49
|
+
# into an otherwise-allowed DTO (defense in depth beyond the type check).
|
|
50
|
+
_FORBIDDEN_FIELD_NAMES = {
|
|
51
|
+
"body",
|
|
52
|
+
"source",
|
|
53
|
+
"source_code",
|
|
54
|
+
"raw_source",
|
|
55
|
+
"file_contents",
|
|
56
|
+
"embedding",
|
|
57
|
+
"embeddings",
|
|
58
|
+
"vector",
|
|
59
|
+
"vectors",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class NetGuardViolation(RuntimeError):
|
|
64
|
+
"""Raised when an outbound payload would leak raw source or source vectors."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def assert_safe_to_send(payload: BaseModel | dict[str, Any] | list[Any]) -> None:
|
|
68
|
+
"""Raise ``NetGuardViolation`` if ``payload`` is unsafe to cross the network boundary.
|
|
69
|
+
|
|
70
|
+
Call this immediately before every ``api_client`` HTTP call.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
_check(payload, path="$")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check(value: Any, *, path: str) -> None:
|
|
77
|
+
if isinstance(value, BaseModel):
|
|
78
|
+
type_name = type(value).__name__
|
|
79
|
+
if type_name not in _ALLOWED_DTO_NAMES:
|
|
80
|
+
raise NetGuardViolation(
|
|
81
|
+
f"net_guard: disallowed DTO type {type_name!r} at {path} "
|
|
82
|
+
"is not part of contracts/openapi.yaml"
|
|
83
|
+
)
|
|
84
|
+
_check(value.model_dump(mode="python"), path=path)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if isinstance(value, dict):
|
|
88
|
+
for key, sub_value in value.items():
|
|
89
|
+
key_lower = str(key).lower()
|
|
90
|
+
if key_lower in _FORBIDDEN_FIELD_NAMES:
|
|
91
|
+
raise NetGuardViolation(f"net_guard: forbidden field {key!r} at {path}")
|
|
92
|
+
_check(sub_value, path=f"{path}.{key}")
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if isinstance(value, (list, tuple, set)):
|
|
96
|
+
for i, item in enumerate(value):
|
|
97
|
+
_check(item, path=f"{path}[{i}]")
|
|
98
|
+
return
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Client for POST /v1/promote (contracts/openapi.yaml).
|
|
2
|
+
|
|
3
|
+
Sends condensate (gists + edge structure) to the server for SuperKG
|
|
4
|
+
augmentation and canonical world-model persistence, and returns the
|
|
5
|
+
stamped result. This is a real network call, not an in-process shim -
|
|
6
|
+
`promote` is one of the two named write operations of the single API
|
|
7
|
+
surface (architecture doc S4).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from codepreproc_client.layer1_business.api_client.net_guard import assert_safe_to_send
|
|
15
|
+
from contracts.dtos import (
|
|
16
|
+
Completeness,
|
|
17
|
+
EdgeRef,
|
|
18
|
+
FingerprintAttestation,
|
|
19
|
+
Gist,
|
|
20
|
+
StampedResult,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DEFAULT_TIMEOUT_S = 30.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def promote(
|
|
27
|
+
*,
|
|
28
|
+
base_url: str,
|
|
29
|
+
jwt: str,
|
|
30
|
+
project_id: str,
|
|
31
|
+
gists: list[Gist],
|
|
32
|
+
edges: list[EdgeRef],
|
|
33
|
+
fingerprint_attestation: FingerprintAttestation,
|
|
34
|
+
completeness: Completeness | None = None,
|
|
35
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
36
|
+
) -> StampedResult:
|
|
37
|
+
"""Augment condensate against SuperKG and persist to the canonical world model."""
|
|
38
|
+
|
|
39
|
+
request_body = {
|
|
40
|
+
"project_id": project_id,
|
|
41
|
+
"gists": [g.model_dump(mode="json") for g in gists],
|
|
42
|
+
"edges": [e.model_dump(mode="json") for e in edges],
|
|
43
|
+
"completeness": completeness.model_dump(mode="json") if completeness else None,
|
|
44
|
+
"fingerprint_attestation": fingerprint_attestation.model_dump(mode="json"),
|
|
45
|
+
}
|
|
46
|
+
for gist in gists:
|
|
47
|
+
assert_safe_to_send(gist)
|
|
48
|
+
for edge in edges:
|
|
49
|
+
assert_safe_to_send(edge)
|
|
50
|
+
assert_safe_to_send(fingerprint_attestation)
|
|
51
|
+
|
|
52
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
53
|
+
response = await client.post(
|
|
54
|
+
"/v1/promote",
|
|
55
|
+
json=request_body,
|
|
56
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
57
|
+
)
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
return StampedResult.model_validate(response.json())
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""HTTP client for server-side LLM reasoning/routing, over POST /v1/reason.
|
|
2
|
+
|
|
3
|
+
Replaces the former same-process shim that re-exported
|
|
4
|
+
codepreproc_server.layer3_core.reasoning symbols directly - client code must
|
|
5
|
+
never import codepreproc_server. The actual provider calls (local Qwen,
|
|
6
|
+
OpenAI, ...), cost tracking, and fallback logic all now run server-side;
|
|
7
|
+
this module only builds requests and validates typed responses.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Awaitable, Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic import BaseModel
|
|
17
|
+
|
|
18
|
+
from contracts.dtos import (
|
|
19
|
+
ROUTING_MATRIX,
|
|
20
|
+
IntentSpec,
|
|
21
|
+
LLMPolicy,
|
|
22
|
+
LLMUsageStats,
|
|
23
|
+
ReasonRequest,
|
|
24
|
+
TaskType,
|
|
25
|
+
)
|
|
26
|
+
from contracts.dtos import ChunkRelevanceBatch, CompactedContext, OperationStub, StubsResponse
|
|
27
|
+
|
|
28
|
+
DEFAULT_TIMEOUT_S = 60.0
|
|
29
|
+
|
|
30
|
+
_SCHEMA_BY_NAME: dict[str, type[BaseModel]] = {
|
|
31
|
+
"IntentSpec": IntentSpec,
|
|
32
|
+
"ChunkRelevanceBatch": ChunkRelevanceBatch,
|
|
33
|
+
"CompactedContext": CompactedContext,
|
|
34
|
+
"StubsResponse": StubsResponse,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProviderUnavailableError(RuntimeError):
|
|
39
|
+
"""Raised when the server-side reasoning call fails (mirrors the server's error)."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LLMRouter:
|
|
43
|
+
"""HTTP-backed stand-in for the server's LLMRouter.chat() - same call
|
|
44
|
+
signature, but the routing/provider/fallback decision now happens
|
|
45
|
+
server-side, behind the JWT-gated POST /v1/reason endpoint."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, base_url: str, jwt_provider: Callable[[], Awaitable[str]], timeout: float = DEFAULT_TIMEOUT_S) -> None:
|
|
48
|
+
self._base_url = base_url
|
|
49
|
+
self._jwt_provider = jwt_provider
|
|
50
|
+
self._timeout = timeout
|
|
51
|
+
|
|
52
|
+
async def chat(
|
|
53
|
+
self,
|
|
54
|
+
task_type: TaskType,
|
|
55
|
+
policy: LLMPolicy,
|
|
56
|
+
messages: list[dict[str, Any]],
|
|
57
|
+
schema: type[BaseModel] | None = None,
|
|
58
|
+
*,
|
|
59
|
+
max_tokens: int = 1024,
|
|
60
|
+
temperature: float = 0.1,
|
|
61
|
+
project_id: str | None = None,
|
|
62
|
+
task_id: str | None = None,
|
|
63
|
+
) -> tuple[BaseModel | dict[str, Any], LLMUsageStats]:
|
|
64
|
+
request = ReasonRequest(
|
|
65
|
+
task_type=task_type,
|
|
66
|
+
policy=policy,
|
|
67
|
+
messages=messages,
|
|
68
|
+
schema_name=schema.__name__ if schema is not None else None,
|
|
69
|
+
max_tokens=max_tokens,
|
|
70
|
+
temperature=temperature,
|
|
71
|
+
project_id=project_id,
|
|
72
|
+
task_id=task_id,
|
|
73
|
+
)
|
|
74
|
+
jwt = await self._jwt_provider()
|
|
75
|
+
try:
|
|
76
|
+
async with httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout) as client:
|
|
77
|
+
response = await client.post(
|
|
78
|
+
"/v1/reason",
|
|
79
|
+
json=request.model_dump(mode="json"),
|
|
80
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
except httpx.HTTPError as exc:
|
|
84
|
+
raise ProviderUnavailableError(str(exc)) from exc
|
|
85
|
+
|
|
86
|
+
body = response.json()
|
|
87
|
+
usage = LLMUsageStats.model_validate(body["usage"])
|
|
88
|
+
result = schema.model_validate(body["result"]) if schema is not None else body["result"]
|
|
89
|
+
return result, usage
|
|
90
|
+
|
|
91
|
+
async def health_check(self, provider_name: str = "local_qwen") -> bool:
|
|
92
|
+
# Provider health now lives entirely server-side; a reachable /v1/reason
|
|
93
|
+
# call is the only signal the client can observe.
|
|
94
|
+
return True
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class LLMRuntime:
|
|
98
|
+
"""HTTP-backed stand-in for the server's LLMRuntime - owns nothing local,
|
|
99
|
+
just a router pointed at codepreproc-server."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, base_url: str, jwt_provider: Callable[[], Awaitable[str]]) -> None:
|
|
102
|
+
self.router = LLMRouter(base_url, jwt_provider)
|
|
103
|
+
|
|
104
|
+
async def close(self) -> None:
|
|
105
|
+
pass # no local provider connections to release
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class IntentExtractor:
|
|
109
|
+
"""Extract structured intent from a user prompt (ported from the server's
|
|
110
|
+
IntentExtractor - depends only on router.chat(), never on server internals)."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, router: LLMRouter) -> None:
|
|
113
|
+
self.router = router
|
|
114
|
+
|
|
115
|
+
async def extract(
|
|
116
|
+
self,
|
|
117
|
+
user_prompt: str,
|
|
118
|
+
project_context: str,
|
|
119
|
+
*,
|
|
120
|
+
policy: LLMPolicy,
|
|
121
|
+
project_id: str,
|
|
122
|
+
task_id: str,
|
|
123
|
+
) -> tuple[IntentSpec, LLMUsageStats]:
|
|
124
|
+
system_prompt = (
|
|
125
|
+
"You are a code change intent extractor. Given a user request about a codebase, "
|
|
126
|
+
"extract a structured intent. Be conservative. Output strict JSON.\n"
|
|
127
|
+
"If the request is about moving or renaming files/directories, or reorganizing the repo tree, "
|
|
128
|
+
"set artifact_scope=filesystem instead of pretending it is an in-file edit.\n"
|
|
129
|
+
"If the request is about creating a new file that does not yet exist (e.g. a new script, "
|
|
130
|
+
"config file, or module), set artifact_scope=new_file and populate files_to_create with "
|
|
131
|
+
"the target file path(s) relative to the project root. "
|
|
132
|
+
"Also set intent=add in this case.\n\n"
|
|
133
|
+
f"Project context:\n{project_context}"
|
|
134
|
+
)
|
|
135
|
+
try:
|
|
136
|
+
result, usage = await self.router.chat(
|
|
137
|
+
task_type="intent_extraction",
|
|
138
|
+
policy=policy,
|
|
139
|
+
messages=[
|
|
140
|
+
{"role": "system", "content": system_prompt},
|
|
141
|
+
{"role": "user", "content": user_prompt},
|
|
142
|
+
],
|
|
143
|
+
schema=IntentSpec,
|
|
144
|
+
max_tokens=500,
|
|
145
|
+
temperature=0.1,
|
|
146
|
+
project_id=project_id,
|
|
147
|
+
task_id=task_id,
|
|
148
|
+
)
|
|
149
|
+
return IntentSpec.model_validate(result) if not isinstance(result, IntentSpec) else result, usage
|
|
150
|
+
except Exception:
|
|
151
|
+
return IntentSpec(change_summary=user_prompt, constraints=[]), LLMUsageStats(provider="fallback", task_type="intent_extraction")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = [
|
|
155
|
+
"IntentExtractor",
|
|
156
|
+
"LLMRuntime",
|
|
157
|
+
"ROUTING_MATRIX",
|
|
158
|
+
"LLMRouter",
|
|
159
|
+
"ChunkRelevanceBatch",
|
|
160
|
+
"CompactedContext",
|
|
161
|
+
"OperationStub",
|
|
162
|
+
"StubsResponse",
|
|
163
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Client for the registry/config surface: /v1/config/defaults, /v1/projects,
|
|
2
|
+
/v1/projects/{id}/superkg_id, /v1/snippets (contracts/openapi.yaml).
|
|
3
|
+
|
|
4
|
+
Real network calls, not an in-process shim - the shared embedding/retrieval/
|
|
5
|
+
chunking defaults, per-project config, and the shared snippet library all
|
|
6
|
+
live in codepreproc-server's Postgres and cross the same JWT-gated boundary
|
|
7
|
+
as promote/lease_world_model (architecture doc S4). Client code must never
|
|
8
|
+
import codepreproc_server or asyncpg directly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from codepreproc_client.layer1_business.api_client.net_guard import assert_safe_to_send
|
|
16
|
+
from contracts.dtos import (
|
|
17
|
+
DefaultsConfig,
|
|
18
|
+
FingerprintAttestation,
|
|
19
|
+
ProjectConfig,
|
|
20
|
+
RegisterProjectRequest,
|
|
21
|
+
SetSuperkgIdRequest,
|
|
22
|
+
SnippetSpec,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
DEFAULT_TIMEOUT_S = 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def get_defaults(*, base_url: str, jwt: str, timeout: float = DEFAULT_TIMEOUT_S) -> DefaultsConfig:
|
|
29
|
+
"""Fetch the shared embedding/retrieval/chunking/LLM-provider defaults."""
|
|
30
|
+
|
|
31
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
32
|
+
response = await client.get(
|
|
33
|
+
"/v1/config/defaults",
|
|
34
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return DefaultsConfig.model_validate(response.json())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def register_project(
|
|
41
|
+
*,
|
|
42
|
+
base_url: str,
|
|
43
|
+
jwt: str,
|
|
44
|
+
project: ProjectConfig,
|
|
45
|
+
fingerprint_attestation: FingerprintAttestation,
|
|
46
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
47
|
+
) -> ProjectConfig:
|
|
48
|
+
"""Register (or update) a project's config. The JWT must already name
|
|
49
|
+
``project.project_id`` in its claims - mint it with this project's
|
|
50
|
+
``fingerprint_attestation`` first."""
|
|
51
|
+
|
|
52
|
+
request = RegisterProjectRequest(project=project, fingerprint_attestation=fingerprint_attestation)
|
|
53
|
+
assert_safe_to_send(request)
|
|
54
|
+
|
|
55
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
56
|
+
response = await client.post(
|
|
57
|
+
"/v1/projects",
|
|
58
|
+
json=request.model_dump(mode="json"),
|
|
59
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return ProjectConfig.model_validate(response.json()["project"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def set_superkg_id(
|
|
66
|
+
*,
|
|
67
|
+
base_url: str,
|
|
68
|
+
jwt: str,
|
|
69
|
+
project_id: str,
|
|
70
|
+
system_id: str,
|
|
71
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
72
|
+
) -> None:
|
|
73
|
+
request = SetSuperkgIdRequest(system_id=system_id)
|
|
74
|
+
assert_safe_to_send(request)
|
|
75
|
+
|
|
76
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
77
|
+
response = await client.patch(
|
|
78
|
+
f"/v1/projects/{project_id}/superkg_id",
|
|
79
|
+
json=request.model_dump(mode="json"),
|
|
80
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def list_projects(*, base_url: str, jwt: str, timeout: float = DEFAULT_TIMEOUT_S) -> list[ProjectConfig]:
|
|
86
|
+
"""List every project the caller's tenant (from the JWT's claims) is allowed to see.
|
|
87
|
+
|
|
88
|
+
Callers should cache this locally (allowed_scope_cache.py) rather than
|
|
89
|
+
calling it on every action - it only needs to be re-fetched when the
|
|
90
|
+
cache is empty or the server rejects a project_id as unauthorized."""
|
|
91
|
+
|
|
92
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
93
|
+
response = await client.get(
|
|
94
|
+
"/v1/projects",
|
|
95
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
96
|
+
)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
return [ProjectConfig.model_validate(p) for p in response.json()["projects"]]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def list_snippets(
|
|
102
|
+
*,
|
|
103
|
+
base_url: str,
|
|
104
|
+
jwt: str,
|
|
105
|
+
framework: str | None = None,
|
|
106
|
+
language: str | None = None,
|
|
107
|
+
layer: str | None = None,
|
|
108
|
+
timeout: float = DEFAULT_TIMEOUT_S,
|
|
109
|
+
) -> list[SnippetSpec]:
|
|
110
|
+
params = {k: v for k, v in {"framework": framework, "language": language, "layer": layer}.items() if v is not None}
|
|
111
|
+
|
|
112
|
+
async with httpx.AsyncClient(base_url=base_url, timeout=timeout) as client:
|
|
113
|
+
response = await client.get(
|
|
114
|
+
"/v1/snippets",
|
|
115
|
+
params=params,
|
|
116
|
+
headers={"Authorization": f"Bearer {jwt}"},
|
|
117
|
+
)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
return [SnippetSpec.model_validate(s) for s in response.json()["snippets"]]
|