dbl-gateway 0.3.2__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.
- dbl_gateway/__init__.py +1 -0
- dbl_gateway/adapters/__init__.py +9 -0
- dbl_gateway/adapters/execution_adapter_kl.py +133 -0
- dbl_gateway/adapters/policy_adapter_dbl_policy.py +96 -0
- dbl_gateway/adapters/store_adapter_sqlite.py +55 -0
- dbl_gateway/admission.py +67 -0
- dbl_gateway/app.py +501 -0
- dbl_gateway/auth.py +295 -0
- dbl_gateway/capabilities.py +79 -0
- dbl_gateway/digest.py +31 -0
- dbl_gateway/execution.py +15 -0
- dbl_gateway/governance.py +20 -0
- dbl_gateway/models.py +24 -0
- dbl_gateway/ports/__init__.py +11 -0
- dbl_gateway/ports/execution_port.py +19 -0
- dbl_gateway/ports/policy_port.py +18 -0
- dbl_gateway/ports/store_port.py +33 -0
- dbl_gateway/projection.py +34 -0
- dbl_gateway/providers/__init__.py +1 -0
- dbl_gateway/providers/anthropic.py +63 -0
- dbl_gateway/providers/errors.py +5 -0
- dbl_gateway/providers/openai.py +105 -0
- dbl_gateway/store/__init__.py +1 -0
- dbl_gateway/store/base.py +35 -0
- dbl_gateway/store/factory.py +12 -0
- dbl_gateway/store/sqlite.py +200 -0
- dbl_gateway/wire_contract.py +65 -0
- dbl_gateway-0.3.2.dist-info/METADATA +78 -0
- dbl_gateway-0.3.2.dist-info/RECORD +33 -0
- dbl_gateway-0.3.2.dist-info/WHEEL +5 -0
- dbl_gateway-0.3.2.dist-info/entry_points.txt +2 -0
- dbl_gateway-0.3.2.dist-info/licenses/LICENSE +21 -0
- dbl_gateway-0.3.2.dist-info/top_level.txt +1 -0
dbl_gateway/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from dbl_core import normalize_trace
|
|
8
|
+
|
|
9
|
+
from ..ports.execution_port import ExecutionPort, ExecutionResult
|
|
10
|
+
from ..providers import anthropic, openai
|
|
11
|
+
from ..providers.errors import ProviderError
|
|
12
|
+
from ..capabilities import resolve_provider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class KlExecutionAdapter(ExecutionPort):
|
|
17
|
+
async def run(self, intent_event: Mapping[str, Any]) -> ExecutionResult:
|
|
18
|
+
payload = intent_event.get("payload")
|
|
19
|
+
if not isinstance(payload, Mapping):
|
|
20
|
+
return ExecutionResult(error={"message": "invalid payload"})
|
|
21
|
+
requested_model_id = payload.get("requested_model_id")
|
|
22
|
+
model_id = str(requested_model_id) if requested_model_id else ""
|
|
23
|
+
provider, reason = resolve_provider(model_id)
|
|
24
|
+
if provider is None or reason is not None:
|
|
25
|
+
return ExecutionResult(
|
|
26
|
+
provider=provider,
|
|
27
|
+
model_id=model_id,
|
|
28
|
+
error={"provider": provider, "message": reason or "model.unavailable"},
|
|
29
|
+
)
|
|
30
|
+
message = _extract_message(payload)
|
|
31
|
+
if message is None:
|
|
32
|
+
return ExecutionResult(provider=provider, model_id=model_id, error={"message": "input.invalid"})
|
|
33
|
+
call = _select_provider(provider)
|
|
34
|
+
try:
|
|
35
|
+
output_text, trace, trace_digest, error = await _call_kernel(message, model_id, provider, call)
|
|
36
|
+
return ExecutionResult(
|
|
37
|
+
output_text=output_text,
|
|
38
|
+
provider=provider,
|
|
39
|
+
model_id=model_id,
|
|
40
|
+
trace=trace,
|
|
41
|
+
trace_digest=trace_digest,
|
|
42
|
+
error=error,
|
|
43
|
+
)
|
|
44
|
+
except Exception:
|
|
45
|
+
return ExecutionResult(
|
|
46
|
+
provider=provider,
|
|
47
|
+
model_id=model_id,
|
|
48
|
+
error={
|
|
49
|
+
"provider": provider,
|
|
50
|
+
"message": "execution failed",
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def schedule_execution(coro: asyncio.Task | asyncio.Future | Any) -> None:
|
|
56
|
+
asyncio.create_task(coro)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _select_provider(name: str):
|
|
60
|
+
if name == "openai":
|
|
61
|
+
return openai.execute
|
|
62
|
+
if name == "anthropic":
|
|
63
|
+
return anthropic.execute
|
|
64
|
+
raise RuntimeError("unsupported provider")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _call_kernel(message: str, model_id: str, provider: str, provider_call):
|
|
68
|
+
import kl_kernel_logic
|
|
69
|
+
|
|
70
|
+
psi = kl_kernel_logic.PsiDefinition(
|
|
71
|
+
psi_type="llm",
|
|
72
|
+
name=provider,
|
|
73
|
+
metadata={"model_id": model_id},
|
|
74
|
+
)
|
|
75
|
+
kernel = kl_kernel_logic.Kernel()
|
|
76
|
+
|
|
77
|
+
def _task(message: str, model_id: str) -> dict[str, object]:
|
|
78
|
+
try:
|
|
79
|
+
return {"ok": True, "output": provider_call(message, model_id)}
|
|
80
|
+
except ProviderError as exc:
|
|
81
|
+
return {
|
|
82
|
+
"ok": False,
|
|
83
|
+
"error": {
|
|
84
|
+
"status_code": exc.status_code,
|
|
85
|
+
"code": exc.code,
|
|
86
|
+
"message": str(exc),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
trace = await asyncio.to_thread(
|
|
91
|
+
kernel.execute,
|
|
92
|
+
psi=psi,
|
|
93
|
+
task=_task,
|
|
94
|
+
metadata={"provider": provider, "model_id": model_id},
|
|
95
|
+
message=message,
|
|
96
|
+
model_id=model_id,
|
|
97
|
+
)
|
|
98
|
+
trace_dict, trace_digest_value = normalize_trace(trace)
|
|
99
|
+
if not trace.success:
|
|
100
|
+
return (
|
|
101
|
+
"",
|
|
102
|
+
trace_dict,
|
|
103
|
+
trace_digest_value,
|
|
104
|
+
{
|
|
105
|
+
"provider": provider,
|
|
106
|
+
"message": trace.error or "execution failed",
|
|
107
|
+
"failure_code": getattr(trace.failure_code, "value", None),
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
output = trace.output
|
|
111
|
+
if isinstance(output, dict) and output.get("ok") is False:
|
|
112
|
+
err = output.get("error") if isinstance(output.get("error"), dict) else {}
|
|
113
|
+
return (
|
|
114
|
+
"",
|
|
115
|
+
trace_dict,
|
|
116
|
+
trace_digest_value,
|
|
117
|
+
{
|
|
118
|
+
"provider": provider,
|
|
119
|
+
"status_code": err.get("status_code"),
|
|
120
|
+
"code": err.get("code"),
|
|
121
|
+
"message": str(err.get("message") or "execution failed"),
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
if isinstance(output, dict) and "output" in output:
|
|
125
|
+
return str(output.get("output") or ""), trace_dict, trace_digest_value, None
|
|
126
|
+
return str(output or ""), trace_dict, trace_digest_value, None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _extract_message(payload: Mapping[str, Any]) -> str | None:
|
|
130
|
+
message = payload.get("message")
|
|
131
|
+
if isinstance(message, str) and message.strip():
|
|
132
|
+
return message.strip()
|
|
133
|
+
return None
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
from typing import Any, Mapping, get_type_hints
|
|
6
|
+
|
|
7
|
+
from dbl_policy import Policy, PolicyContext, PolicyDecision, decision_to_dbl_event
|
|
8
|
+
|
|
9
|
+
from ..ports.policy_port import DecisionResult, PolicyPort
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ALLOWED_CONTEXT_KEYS = {
|
|
13
|
+
"stream_id",
|
|
14
|
+
"lane",
|
|
15
|
+
"actor",
|
|
16
|
+
"intent_type",
|
|
17
|
+
"correlation_id",
|
|
18
|
+
"payload",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class DblPolicyAdapter(PolicyPort):
|
|
24
|
+
def decide(self, authoritative_input: Mapping[str, Any]) -> DecisionResult:
|
|
25
|
+
context = _build_policy_context(authoritative_input)
|
|
26
|
+
decision = _evaluate_policy(context)
|
|
27
|
+
gate_event = decision_to_dbl_event(decision, authoritative_input["correlation_id"])
|
|
28
|
+
policy_version = _policy_version_as_int(decision.policy_version.value)
|
|
29
|
+
return DecisionResult(
|
|
30
|
+
decision=decision.outcome.value,
|
|
31
|
+
reason_codes=[decision.reason_code],
|
|
32
|
+
policy_id=decision.policy_id.value,
|
|
33
|
+
policy_version=policy_version,
|
|
34
|
+
gate_event=gate_event,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _build_policy_context(authoritative_input: Mapping[str, Any]) -> PolicyContext:
|
|
39
|
+
filtered = {key: authoritative_input.get(key) for key in ALLOWED_CONTEXT_KEYS}
|
|
40
|
+
tenant = authoritative_input.get("tenant_id", "unknown")
|
|
41
|
+
tenant_type = _tenant_id_type()
|
|
42
|
+
try:
|
|
43
|
+
tenant_value = tenant_type(str(tenant))
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
raise RuntimeError("invalid tenant_id") from exc
|
|
46
|
+
return PolicyContext(tenant_id=tenant_value, inputs=filtered)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _evaluate_policy(context: PolicyContext) -> PolicyDecision:
|
|
50
|
+
policy = _load_policy()
|
|
51
|
+
return policy.evaluate(context)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_policy() -> Policy:
|
|
55
|
+
module_path = _get_env("DBL_GATEWAY_POLICY_MODULE")
|
|
56
|
+
obj_name = _get_env("DBL_GATEWAY_POLICY_OBJECT", "policy")
|
|
57
|
+
module = import_module(module_path)
|
|
58
|
+
obj = getattr(module, obj_name, None)
|
|
59
|
+
if obj is None:
|
|
60
|
+
raise RuntimeError("policy object not found")
|
|
61
|
+
if callable(obj) and not hasattr(obj, "evaluate"):
|
|
62
|
+
return obj() # type: ignore[return-value]
|
|
63
|
+
return obj # type: ignore[return-value]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_env(name: str, default: str | None = None) -> str:
|
|
67
|
+
import os
|
|
68
|
+
|
|
69
|
+
value = os.getenv(name, "")
|
|
70
|
+
if value:
|
|
71
|
+
return value
|
|
72
|
+
if default is None:
|
|
73
|
+
raise RuntimeError(f"{name} is required")
|
|
74
|
+
return default
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _tenant_id_type() -> type:
|
|
78
|
+
hints = get_type_hints(PolicyContext)
|
|
79
|
+
tenant_type = hints.get("tenant_id")
|
|
80
|
+
if not isinstance(tenant_type, type):
|
|
81
|
+
raise RuntimeError("PolicyContext.tenant_id type missing")
|
|
82
|
+
return tenant_type
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _policy_version_as_int(value: object) -> int:
|
|
86
|
+
try:
|
|
87
|
+
if isinstance(value, str):
|
|
88
|
+
text = value.strip()
|
|
89
|
+
if text == "":
|
|
90
|
+
raise ValueError("empty")
|
|
91
|
+
if "." in text:
|
|
92
|
+
text = text.split(".", 1)[0]
|
|
93
|
+
return int(text)
|
|
94
|
+
return int(value)
|
|
95
|
+
except (TypeError, ValueError) as exc:
|
|
96
|
+
raise RuntimeError("policy_version must be int") from exc
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..ports.store_port import StorePort
|
|
7
|
+
from ..store.sqlite import SQLiteStore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class SQLiteStoreAdapter(StorePort):
|
|
12
|
+
_store: SQLiteStore
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_path(cls, db_path: Path) -> "SQLiteStoreAdapter":
|
|
16
|
+
return cls(SQLiteStore(db_path))
|
|
17
|
+
|
|
18
|
+
def append(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
kind: str,
|
|
22
|
+
lane: str,
|
|
23
|
+
actor: str,
|
|
24
|
+
intent_type: str,
|
|
25
|
+
stream_id: str,
|
|
26
|
+
correlation_id: str,
|
|
27
|
+
payload: dict[str, object],
|
|
28
|
+
):
|
|
29
|
+
return self._store.append(
|
|
30
|
+
kind=kind,
|
|
31
|
+
lane=lane,
|
|
32
|
+
actor=actor,
|
|
33
|
+
intent_type=intent_type,
|
|
34
|
+
stream_id=stream_id,
|
|
35
|
+
correlation_id=correlation_id,
|
|
36
|
+
payload=payload,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def snapshot(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
limit: int,
|
|
43
|
+
offset: int,
|
|
44
|
+
stream_id: str | None = None,
|
|
45
|
+
lane: str | None = None,
|
|
46
|
+
):
|
|
47
|
+
return self._store.snapshot(
|
|
48
|
+
limit=limit,
|
|
49
|
+
offset=offset,
|
|
50
|
+
stream_id=stream_id,
|
|
51
|
+
lane=lane,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def close(self) -> None:
|
|
55
|
+
self._store.close()
|
dbl_gateway/admission.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
from dbl_ingress import (
|
|
7
|
+
AdmissionError,
|
|
8
|
+
InvalidInputError,
|
|
9
|
+
AdmissionRecord,
|
|
10
|
+
shape_input,
|
|
11
|
+
ADMISSION_INVALID_INPUT,
|
|
12
|
+
ADMISSION_SECRETS_PRESENT,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SECRET_KEYS = {"api_key", "authorization", "token", "secret", "bearer"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AdmissionFailure(Exception):
|
|
21
|
+
reason_code: str
|
|
22
|
+
detail: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def admit_and_shape_intent(payload: Mapping[str, Any], *, raw_payload: Mapping[str, Any] | None = None) -> AdmissionRecord:
|
|
26
|
+
if raw_payload is not None and _contains_secrets(raw_payload):
|
|
27
|
+
raise AdmissionFailure(reason_code=ADMISSION_SECRETS_PRESENT, detail="secrets detected in payload")
|
|
28
|
+
if _contains_secrets(payload):
|
|
29
|
+
raise AdmissionFailure(reason_code=ADMISSION_SECRETS_PRESENT, detail="secrets detected in payload")
|
|
30
|
+
|
|
31
|
+
correlation_id = payload.get("correlation_id")
|
|
32
|
+
deterministic = payload.get("deterministic")
|
|
33
|
+
observational = payload.get("observational")
|
|
34
|
+
|
|
35
|
+
if not isinstance(correlation_id, str) or not correlation_id.strip():
|
|
36
|
+
raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="correlation_id must be a non-empty string")
|
|
37
|
+
if not isinstance(deterministic, Mapping):
|
|
38
|
+
raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="deterministic must be an object")
|
|
39
|
+
if observational is not None and not isinstance(observational, Mapping):
|
|
40
|
+
raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="observational must be an object if provided")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
record = shape_input(
|
|
44
|
+
correlation_id=correlation_id,
|
|
45
|
+
deterministic=deterministic,
|
|
46
|
+
observational=observational,
|
|
47
|
+
)
|
|
48
|
+
except InvalidInputError as exc:
|
|
49
|
+
raise AdmissionFailure(reason_code=exc.reason_code, detail=str(exc)) from exc
|
|
50
|
+
except AdmissionError as exc:
|
|
51
|
+
reason = getattr(exc, "reason_code", ADMISSION_INVALID_INPUT)
|
|
52
|
+
raise AdmissionFailure(reason_code=reason, detail=str(exc)) from exc
|
|
53
|
+
return record
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _contains_secrets(value: object) -> bool:
|
|
57
|
+
if isinstance(value, Mapping):
|
|
58
|
+
for key, item in value.items():
|
|
59
|
+
if isinstance(key, str) and key.lower() in SECRET_KEYS:
|
|
60
|
+
if isinstance(item, str) and item.strip():
|
|
61
|
+
return True
|
|
62
|
+
if _contains_secrets(item):
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
if isinstance(value, list):
|
|
66
|
+
return any(_contains_secrets(item) for item in value)
|
|
67
|
+
return False
|