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.
@@ -0,0 +1 @@
1
+ __all__ = ["app"]
@@ -0,0 +1,9 @@
1
+ from .execution_adapter_kl import KlExecutionAdapter
2
+ from .policy_adapter_dbl_policy import DblPolicyAdapter
3
+ from .store_adapter_sqlite import SQLiteStoreAdapter
4
+
5
+ __all__ = [
6
+ "DblPolicyAdapter",
7
+ "KlExecutionAdapter",
8
+ "SQLiteStoreAdapter",
9
+ ]
@@ -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()
@@ -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