monoid-agent-kernel 0.16.1__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.
- monoid_agent_kernel/__init__.py +11 -0
- monoid_agent_kernel/_policy_util.py +40 -0
- monoid_agent_kernel/_proc.py +86 -0
- monoid_agent_kernel/builder.py +351 -0
- monoid_agent_kernel/cli.py +1244 -0
- monoid_agent_kernel/conformance/__init__.py +32 -0
- monoid_agent_kernel/conformance/harness.py +209 -0
- monoid_agent_kernel/conformance/profiles/__init__.py +55 -0
- monoid_agent_kernel/conformance/profiles/_metadata.py +16 -0
- monoid_agent_kernel/conformance/profiles/capability_security.py +216 -0
- monoid_agent_kernel/conformance/profiles/control_plane.py +41 -0
- monoid_agent_kernel/conformance/profiles/durable_runner.py +42 -0
- monoid_agent_kernel/conformance/profiles/message_fabric.py +42 -0
- monoid_agent_kernel/conformance/profiles/minimal_agent.py +13 -0
- monoid_agent_kernel/conformance/profiles/multi_agent.py +112 -0
- monoid_agent_kernel/conformance/profiles/provider_gateway.py +107 -0
- monoid_agent_kernel/conformance/profiles/reference_full.py +135 -0
- monoid_agent_kernel/conformance/profiles/side_effect_tool_agent.py +36 -0
- monoid_agent_kernel/conformance/profiles/tool_agent.py +39 -0
- monoid_agent_kernel/contracts.py +329 -0
- monoid_agent_kernel/core/__init__.py +1 -0
- monoid_agent_kernel/core/_util.py +145 -0
- monoid_agent_kernel/core/agents.py +883 -0
- monoid_agent_kernel/core/cancellation.py +16 -0
- monoid_agent_kernel/core/capability.py +376 -0
- monoid_agent_kernel/core/capability_revocation.py +92 -0
- monoid_agent_kernel/core/checkpoint.py +350 -0
- monoid_agent_kernel/core/content.py +101 -0
- monoid_agent_kernel/core/context.py +67 -0
- monoid_agent_kernel/core/control.py +124 -0
- monoid_agent_kernel/core/control_audit.py +99 -0
- monoid_agent_kernel/core/durable_metadata.py +116 -0
- monoid_agent_kernel/core/event_sequencing.py +164 -0
- monoid_agent_kernel/core/events.py +204 -0
- monoid_agent_kernel/core/external_agent_envelope.py +338 -0
- monoid_agent_kernel/core/frontmatter.py +140 -0
- monoid_agent_kernel/core/inbox.py +110 -0
- monoid_agent_kernel/core/lease_admission.py +57 -0
- monoid_agent_kernel/core/lifecycle.py +440 -0
- monoid_agent_kernel/core/manifest.py +88 -0
- monoid_agent_kernel/core/media.py +393 -0
- monoid_agent_kernel/core/outbox.py +212 -0
- monoid_agent_kernel/core/output_validator.py +103 -0
- monoid_agent_kernel/core/packages.py +742 -0
- monoid_agent_kernel/core/projections.py +213 -0
- monoid_agent_kernel/core/prompt.py +37 -0
- monoid_agent_kernel/core/proposal_file.py +84 -0
- monoid_agent_kernel/core/result.py +131 -0
- monoid_agent_kernel/core/schemas.py +1146 -0
- monoid_agent_kernel/core/scope.py +185 -0
- monoid_agent_kernel/core/side_effect_policy.py +153 -0
- monoid_agent_kernel/core/spec.py +325 -0
- monoid_agent_kernel/core/streaming.py +208 -0
- monoid_agent_kernel/core/subagent_runtime.py +187 -0
- monoid_agent_kernel/core/tool_approval.py +175 -0
- monoid_agent_kernel/core/tool_surface.py +505 -0
- monoid_agent_kernel/core/trace_context.py +76 -0
- monoid_agent_kernel/core/wire_validation.py +156 -0
- monoid_agent_kernel/core/workspace.py +161 -0
- monoid_agent_kernel/core/workspace_index.py +127 -0
- monoid_agent_kernel/env.py +32 -0
- monoid_agent_kernel/errors.py +105 -0
- monoid_agent_kernel/event_loader.py +47 -0
- monoid_agent_kernel/identifiers.py +50 -0
- monoid_agent_kernel/loop.py +4140 -0
- monoid_agent_kernel/loop_phases.py +561 -0
- monoid_agent_kernel/mcp/__init__.py +10 -0
- monoid_agent_kernel/mcp/client.py +227 -0
- monoid_agent_kernel/mcp/provider.py +407 -0
- monoid_agent_kernel/narration.py +92 -0
- monoid_agent_kernel/observability/__init__.py +9 -0
- monoid_agent_kernel/observability/otel.py +264 -0
- monoid_agent_kernel/permissions.py +74 -0
- monoid_agent_kernel/providers/__init__.py +7 -0
- monoid_agent_kernel/providers/_common.py +122 -0
- monoid_agent_kernel/providers/base.py +312 -0
- monoid_agent_kernel/providers/fake.py +76 -0
- monoid_agent_kernel/providers/gateway.py +474 -0
- monoid_agent_kernel/providers/openai.py +487 -0
- monoid_agent_kernel/public_view.py +173 -0
- monoid_agent_kernel/py.typed +0 -0
- monoid_agent_kernel/recorder.py +421 -0
- monoid_agent_kernel/reference/__init__.py +15 -0
- monoid_agent_kernel/reference/_shared/__init__.py +4 -0
- monoid_agent_kernel/reference/_shared/http_util.py +111 -0
- monoid_agent_kernel/reference/_shared/tokens.py +314 -0
- monoid_agent_kernel/reference/backend/__init__.py +21 -0
- monoid_agent_kernel/reference/backend/commands.py +375 -0
- monoid_agent_kernel/reference/backend/http.py +439 -0
- monoid_agent_kernel/reference/backend/jobs.py +68 -0
- monoid_agent_kernel/reference/backend/loop_factory.py +253 -0
- monoid_agent_kernel/reference/backend/outbox_dispatch.py +130 -0
- monoid_agent_kernel/reference/backend/ports.py +188 -0
- monoid_agent_kernel/reference/backend/projection.py +286 -0
- monoid_agent_kernel/reference/backend/proposal.py +220 -0
- monoid_agent_kernel/reference/backend/proposal_reader.py +16 -0
- monoid_agent_kernel/reference/backend/recovery.py +253 -0
- monoid_agent_kernel/reference/backend/run_execution.py +152 -0
- monoid_agent_kernel/reference/backend/run_preparation.py +197 -0
- monoid_agent_kernel/reference/backend/run_state.py +243 -0
- monoid_agent_kernel/reference/backend/run_types.py +128 -0
- monoid_agent_kernel/reference/backend/runtime_config.py +147 -0
- monoid_agent_kernel/reference/backend/service.py +1664 -0
- monoid_agent_kernel/reference/backend/session.py +280 -0
- monoid_agent_kernel/reference/backend/session_drive.py +231 -0
- monoid_agent_kernel/reference/capability.py +101 -0
- monoid_agent_kernel/reference/conformance.py +1777 -0
- monoid_agent_kernel/reference/llm_gateway/__init__.py +14 -0
- monoid_agent_kernel/reference/llm_gateway/http.py +225 -0
- monoid_agent_kernel/reference/llm_gateway/providers.py +96 -0
- monoid_agent_kernel/reference/llm_gateway/service.py +404 -0
- monoid_agent_kernel/reference/mcp_gateway/__init__.py +26 -0
- monoid_agent_kernel/reference/mcp_gateway/http.py +187 -0
- monoid_agent_kernel/reference/mcp_gateway/service.py +206 -0
- monoid_agent_kernel/reference/outbox.py +135 -0
- monoid_agent_kernel/reference/stores/__init__.py +20 -0
- monoid_agent_kernel/reference/stores/lease.py +116 -0
- monoid_agent_kernel/reference/stores/sqlite.py +295 -0
- monoid_agent_kernel/reference/studio/README.md +97 -0
- monoid_agent_kernel/reference/studio/__init__.py +21 -0
- monoid_agent_kernel/reference/studio/activity.py +53 -0
- monoid_agent_kernel/reference/studio/cli.py +481 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/SKILL.md +25 -0
- monoid_agent_kernel/reference/studio/sample-skills/code-review-checklist/references/review-checklist.md +8 -0
- monoid_agent_kernel/reference/studio/sample-skills/commit-message/SKILL.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/incident-summary/references/incident-template.md +32 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/SKILL.md +24 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/references/release-note-template.md +33 -0
- monoid_agent_kernel/reference/studio/sample-skills/release-notes/scripts/collect_changes.py +51 -0
- monoid_agent_kernel/reference/studio/server.py +1922 -0
- monoid_agent_kernel/reference/studio/web/index.html +1877 -0
- monoid_agent_kernel/reference/studio/web/settings.html +108 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/auto-render.min.js +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.css +1 -0
- monoid_agent_kernel/reference/studio/web/vendor/katex/katex.min.js +1 -0
- monoid_agent_kernel/reference/studio/window.py +72 -0
- monoid_agent_kernel/reference/web_gateway/__init__.py +6 -0
- monoid_agent_kernel/reference/web_gateway/http.py +155 -0
- monoid_agent_kernel/reference/web_gateway/providers.py +807 -0
- monoid_agent_kernel/reference/web_gateway/service.py +559 -0
- monoid_agent_kernel/shell.py +722 -0
- monoid_agent_kernel/skills/__init__.py +20 -0
- monoid_agent_kernel/skills/definition.py +82 -0
- monoid_agent_kernel/skills/loader.py +55 -0
- monoid_agent_kernel/skills/provider.py +378 -0
- monoid_agent_kernel/subagent_loader.py +56 -0
- monoid_agent_kernel/tasks.py +1326 -0
- monoid_agent_kernel/tool_loader.py +51 -0
- monoid_agent_kernel/tool_services/__init__.py +8 -0
- monoid_agent_kernel/tool_services/base.py +25 -0
- monoid_agent_kernel/tool_services/jobs.py +47 -0
- monoid_agent_kernel/tool_services/shell.py +255 -0
- monoid_agent_kernel/tool_services/web.py +356 -0
- monoid_agent_kernel/tools/__init__.py +2 -0
- monoid_agent_kernel/tools/base.py +205 -0
- monoid_agent_kernel/tools/builtin.py +958 -0
- monoid_agent_kernel/tools/decorator.py +132 -0
- monoid_agent_kernel/tools/tool_ids.py +62 -0
- monoid_agent_kernel/web.py +171 -0
- monoid_agent_kernel/workspace/__init__.py +2 -0
- monoid_agent_kernel/workspace/local.py +1004 -0
- monoid_agent_kernel/workspace/paths.py +30 -0
- monoid_agent_kernel-0.16.1.dist-info/METADATA +709 -0
- monoid_agent_kernel-0.16.1.dist-info/RECORD +191 -0
- monoid_agent_kernel-0.16.1.dist-info/WHEEL +4 -0
- monoid_agent_kernel-0.16.1.dist-info/entry_points.txt +3 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/LICENSE +202 -0
- monoid_agent_kernel-0.16.1.dist-info/licenses/NOTICE +8 -0
- native_agent_runner/__init__.py +103 -0
- native_agent_runner/py.typed +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Reference implementation of the Monoid Agent Kernel LLM gateway contract.
|
|
2
|
+
|
|
3
|
+
Provided as an example credential-boundary gateway. Real integrators are expected to build
|
|
4
|
+
their own against ``monoid_agent_kernel.contracts`` and ``docs/CONTRACTS.md``. Not part of
|
|
5
|
+
the supported public surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from monoid_agent_kernel.reference.llm_gateway.service import (
|
|
9
|
+
LlmGatewayBackend,
|
|
10
|
+
LlmGatewayTurnRequest,
|
|
11
|
+
LlmGatewayUsage,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = ["LlmGatewayBackend", "LlmGatewayTurnRequest", "LlmGatewayUsage"]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from http import HTTPStatus
|
|
7
|
+
from http.server import BaseHTTPRequestHandler
|
|
8
|
+
from typing import Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from monoid_agent_kernel.errors import ModelAdapterError, NativeAgentError, PermissionDenied
|
|
12
|
+
from monoid_agent_kernel.reference._shared.http_util import (
|
|
13
|
+
HardenedThreadingHTTPServer,
|
|
14
|
+
HttpRequestTooLarge,
|
|
15
|
+
log_http_request,
|
|
16
|
+
read_json_limited,
|
|
17
|
+
redact_internal_error,
|
|
18
|
+
)
|
|
19
|
+
from monoid_agent_kernel.reference.llm_gateway.service import LlmGatewayBackend
|
|
20
|
+
from monoid_agent_kernel.providers.gateway import (
|
|
21
|
+
GATEWAY_AUTH_ERROR,
|
|
22
|
+
GATEWAY_BAD_REQUEST,
|
|
23
|
+
GATEWAY_BAD_RESPONSE,
|
|
24
|
+
GATEWAY_SERVER_ERROR,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_LOGGER = logging.getLogger("monoid_agent_kernel.llm_gateway.http")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_llm_gateway_handler(
|
|
31
|
+
gateway: LlmGatewayBackend,
|
|
32
|
+
*,
|
|
33
|
+
admin_token: str | None,
|
|
34
|
+
) -> type[BaseHTTPRequestHandler]:
|
|
35
|
+
class LlmGatewayHttpHandler(BaseHTTPRequestHandler):
|
|
36
|
+
server_version = "MonoidLlmGateway/0.2"
|
|
37
|
+
|
|
38
|
+
def do_GET(self) -> None: # noqa: N802
|
|
39
|
+
parsed = urlparse(self.path)
|
|
40
|
+
try:
|
|
41
|
+
if parsed.path == "/healthz":
|
|
42
|
+
self._write_json({"ok": True})
|
|
43
|
+
return
|
|
44
|
+
parts = [part for part in parsed.path.split("/") if part]
|
|
45
|
+
if len(parts) == 5 and parts[:3] == ["internal", "llm", "tenants"] and parts[4] == "usage":
|
|
46
|
+
self._require_admin()
|
|
47
|
+
self._write_json(gateway.tenant_usage(parts[3]))
|
|
48
|
+
return
|
|
49
|
+
self._write_error(HTTPStatus.NOT_FOUND, "not found")
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
self._write_exception(exc)
|
|
52
|
+
|
|
53
|
+
def do_POST(self) -> None: # noqa: N802
|
|
54
|
+
parsed = urlparse(self.path)
|
|
55
|
+
try:
|
|
56
|
+
if parsed.path == "/internal/llm/turns":
|
|
57
|
+
self._write_json(gateway.handle_turn(self._bearer_token(), self._read_json()))
|
|
58
|
+
return
|
|
59
|
+
if parsed.path == "/internal/llm/turns/stream":
|
|
60
|
+
# Auth/parse/build run eagerly inside handle_turn_stream and may raise
|
|
61
|
+
# here, before any SSE byte — those map to a normal error response below.
|
|
62
|
+
frames = gateway.handle_turn_stream(self._bearer_token(), self._read_json())
|
|
63
|
+
self._write_sse(frames)
|
|
64
|
+
return
|
|
65
|
+
self._write_error(HTTPStatus.NOT_FOUND, "not found")
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
self._write_exception(exc)
|
|
68
|
+
|
|
69
|
+
def log_request(self, code: Any = "-", size: Any = "-") -> None: # noqa: ARG002
|
|
70
|
+
log_http_request(_LOGGER, self, code)
|
|
71
|
+
|
|
72
|
+
def log_message(self, _format: str, *_args: Any) -> None:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def _read_json(self) -> dict[str, Any]:
|
|
76
|
+
return read_json_limited(self)
|
|
77
|
+
|
|
78
|
+
def _bearer_token(self) -> str:
|
|
79
|
+
header = self.headers.get("Authorization") or ""
|
|
80
|
+
prefix = "Bearer "
|
|
81
|
+
if not header.startswith(prefix):
|
|
82
|
+
raise PermissionDenied("missing bearer token")
|
|
83
|
+
return header[len(prefix) :].strip()
|
|
84
|
+
|
|
85
|
+
def _require_admin(self) -> None:
|
|
86
|
+
if admin_token is None:
|
|
87
|
+
raise PermissionDenied("admin token is not configured")
|
|
88
|
+
if self._bearer_token() != admin_token:
|
|
89
|
+
raise PermissionDenied("invalid admin token")
|
|
90
|
+
|
|
91
|
+
def _write_exception(self, exc: Exception) -> None:
|
|
92
|
+
if isinstance(exc, PermissionDenied):
|
|
93
|
+
self._write_error(
|
|
94
|
+
HTTPStatus.UNAUTHORIZED,
|
|
95
|
+
str(exc),
|
|
96
|
+
error_code=GATEWAY_AUTH_ERROR,
|
|
97
|
+
retryable=False,
|
|
98
|
+
)
|
|
99
|
+
elif isinstance(exc, ModelAdapterError):
|
|
100
|
+
status = _model_error_status(exc)
|
|
101
|
+
self._write_error(
|
|
102
|
+
status,
|
|
103
|
+
str(exc),
|
|
104
|
+
error_code=exc.provider_error_code or GATEWAY_BAD_RESPONSE,
|
|
105
|
+
retryable=exc.retryable,
|
|
106
|
+
)
|
|
107
|
+
elif isinstance(exc, HttpRequestTooLarge):
|
|
108
|
+
self._write_error(
|
|
109
|
+
HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
|
110
|
+
str(exc),
|
|
111
|
+
error_code=GATEWAY_BAD_REQUEST,
|
|
112
|
+
retryable=False,
|
|
113
|
+
)
|
|
114
|
+
elif isinstance(exc, ValueError):
|
|
115
|
+
self._write_error(
|
|
116
|
+
HTTPStatus.BAD_REQUEST,
|
|
117
|
+
str(exc),
|
|
118
|
+
error_code=GATEWAY_BAD_REQUEST,
|
|
119
|
+
retryable=False,
|
|
120
|
+
)
|
|
121
|
+
elif isinstance(exc, NativeAgentError):
|
|
122
|
+
self._write_error(
|
|
123
|
+
HTTPStatus.BAD_REQUEST,
|
|
124
|
+
str(exc),
|
|
125
|
+
error_code=getattr(exc, "error_code", GATEWAY_BAD_REQUEST),
|
|
126
|
+
retryable=False,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
self._write_error(
|
|
130
|
+
HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
131
|
+
redact_internal_error(_LOGGER, self, exc),
|
|
132
|
+
error_code=GATEWAY_SERVER_ERROR,
|
|
133
|
+
retryable=True,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _write_error(
|
|
137
|
+
self,
|
|
138
|
+
status: HTTPStatus,
|
|
139
|
+
message: str,
|
|
140
|
+
*,
|
|
141
|
+
error_code: str = GATEWAY_BAD_RESPONSE,
|
|
142
|
+
retryable: bool = False,
|
|
143
|
+
) -> None:
|
|
144
|
+
self._write_json(
|
|
145
|
+
{
|
|
146
|
+
"error": message,
|
|
147
|
+
"error_code": error_code,
|
|
148
|
+
"retryable": retryable,
|
|
149
|
+
"http_status": int(status),
|
|
150
|
+
},
|
|
151
|
+
status=status,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _write_json(self, payload: dict[str, Any], *, status: HTTPStatus = HTTPStatus.OK) -> None:
|
|
155
|
+
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
156
|
+
self.send_response(int(status))
|
|
157
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
158
|
+
self.send_header("Content-Length", str(len(body)))
|
|
159
|
+
self.end_headers()
|
|
160
|
+
self.wfile.write(body)
|
|
161
|
+
|
|
162
|
+
def _write_sse(self, frames: Iterable[dict[str, Any]]) -> None:
|
|
163
|
+
# A streaming turn is expected to be long-lived, so clear this route's 30s socket
|
|
164
|
+
# timeout (which would otherwise kill the connection on a >30s gap between tokens).
|
|
165
|
+
# The provider call's own timeout still bounds a wedged upstream. Other routes keep
|
|
166
|
+
# the default. No Content-Length: the body is unbounded and ends on connection close.
|
|
167
|
+
self.connection.settimeout(None)
|
|
168
|
+
self.send_response(int(HTTPStatus.OK))
|
|
169
|
+
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
170
|
+
self.send_header("Cache-Control", "no-cache")
|
|
171
|
+
self.send_header("Connection", "close")
|
|
172
|
+
self.end_headers()
|
|
173
|
+
try:
|
|
174
|
+
for frame in frames:
|
|
175
|
+
self._write_sse_frame(frame)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
# We are already committed to a 200 SSE body, so a mid-stream failure surfaces
|
|
178
|
+
# as a terminal error frame (the client raises ModelAdapterError from it).
|
|
179
|
+
self._write_sse_frame(_stream_error_frame(self, exc))
|
|
180
|
+
|
|
181
|
+
def _write_sse_frame(self, frame: dict[str, Any]) -> None:
|
|
182
|
+
# Single-line JSON (no indent), flushed per frame so the stream is live.
|
|
183
|
+
self.wfile.write(b"data: " + json.dumps(frame, ensure_ascii=False).encode("utf-8") + b"\n\n")
|
|
184
|
+
self.wfile.flush()
|
|
185
|
+
|
|
186
|
+
return LlmGatewayHttpHandler
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _stream_error_frame(handler: BaseHTTPRequestHandler, exc: Exception) -> dict[str, Any]:
|
|
190
|
+
"""Mid-stream error as an SSE frame, mirroring ``_write_error``'s fields so the client maps
|
|
191
|
+
it back to a ModelAdapterError identically to a non-200 response."""
|
|
192
|
+
if isinstance(exc, ModelAdapterError):
|
|
193
|
+
return {
|
|
194
|
+
"type": "error",
|
|
195
|
+
"error": str(exc),
|
|
196
|
+
"error_code": exc.provider_error_code or GATEWAY_BAD_RESPONSE,
|
|
197
|
+
"retryable": exc.retryable,
|
|
198
|
+
"http_status": int(_model_error_status(exc)),
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
"type": "error",
|
|
202
|
+
"error": redact_internal_error(_LOGGER, handler, exc),
|
|
203
|
+
"error_code": GATEWAY_SERVER_ERROR,
|
|
204
|
+
"retryable": True,
|
|
205
|
+
"http_status": int(HTTPStatus.INTERNAL_SERVER_ERROR),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _model_error_status(exc: ModelAdapterError) -> HTTPStatus:
|
|
210
|
+
if exc.http_status is not None and 400 <= exc.http_status <= 599:
|
|
211
|
+
try:
|
|
212
|
+
return HTTPStatus(exc.http_status)
|
|
213
|
+
except ValueError:
|
|
214
|
+
pass
|
|
215
|
+
return HTTPStatus.SERVICE_UNAVAILABLE if exc.retryable else HTTPStatus.BAD_GATEWAY
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def create_llm_gateway_server(
|
|
219
|
+
gateway: LlmGatewayBackend,
|
|
220
|
+
*,
|
|
221
|
+
host: str,
|
|
222
|
+
port: int,
|
|
223
|
+
admin_token: str,
|
|
224
|
+
) -> HardenedThreadingHTTPServer:
|
|
225
|
+
return HardenedThreadingHTTPServer((host, port), make_llm_gateway_handler(gateway, admin_token=admin_token))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Reference LLM providers for the gateway — including a key-less offline provider.
|
|
2
|
+
|
|
3
|
+
The gateway's provider seam is a ``ProviderAdapterFactory`` (``LlmGatewayBackend
|
|
4
|
+
.provider_adapter_factory``). When none is supplied the gateway hard-defaults to
|
|
5
|
+
``OpenAIModelAdapter``, so a from-scratch local run needs a real OpenAI key. This module is the
|
|
6
|
+
LLM-side counterpart of ``reference/web_gateway/providers.py`` (``FakeWebProvider`` / ``--provider
|
|
7
|
+
fake``): :func:`offline_provider_factory` lets the gateway answer turns with **zero credentials**,
|
|
8
|
+
so the whole stack — chat, streaming, multi-turn — works offline for local dev and tests.
|
|
9
|
+
|
|
10
|
+
Reference example.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from monoid_agent_kernel.core.spec import ModelConfig
|
|
19
|
+
from monoid_agent_kernel.providers.base import ModelAdapter, ModelRequest, ModelTurn
|
|
20
|
+
from monoid_agent_kernel.reference._shared.tokens import TokenClaims
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _content_text(content: Any) -> str:
|
|
24
|
+
"""Best-effort plain text from a message ``content`` (str or content-part list)."""
|
|
25
|
+
if isinstance(content, str):
|
|
26
|
+
return content.strip()
|
|
27
|
+
if isinstance(content, (list, tuple)):
|
|
28
|
+
parts: list[str] = []
|
|
29
|
+
for part in content:
|
|
30
|
+
if isinstance(part, str):
|
|
31
|
+
parts.append(part)
|
|
32
|
+
elif isinstance(part, dict) and isinstance(part.get("text"), str):
|
|
33
|
+
parts.append(part["text"])
|
|
34
|
+
return "\n".join(parts).strip()
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _latest_user_text(request: ModelRequest) -> str:
|
|
39
|
+
"""The newest user message — preferring the by-value log, then the instruction."""
|
|
40
|
+
if request.messages:
|
|
41
|
+
for message in reversed(request.messages):
|
|
42
|
+
if message.get("role") == "user":
|
|
43
|
+
text = _content_text(message.get("content"))
|
|
44
|
+
if text:
|
|
45
|
+
return text
|
|
46
|
+
return (request.instruction or "").strip()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _estimate_tokens(text: str) -> int:
|
|
50
|
+
return max(1, len(text) // 4)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class EchoModelAdapter:
|
|
55
|
+
"""A zero-dependency offline model: it turns the latest user message into a final-text
|
|
56
|
+
turn, so chat + streaming + multi-turn settle end to end with no network and no key.
|
|
57
|
+
|
|
58
|
+
It never emits tool calls, so a tool-bound run simply gets a conversational reply rather
|
|
59
|
+
than performing work — switch to a real provider to exercise the agentic tool path.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
model: ModelConfig | None = None
|
|
63
|
+
|
|
64
|
+
def next_turn(self, request: ModelRequest) -> ModelTurn:
|
|
65
|
+
user_text = _latest_user_text(request)
|
|
66
|
+
if user_text:
|
|
67
|
+
body = (
|
|
68
|
+
"**Offline echo model** — no provider key is configured, so I can't do real "
|
|
69
|
+
"reasoning or run tools yet.\n\n"
|
|
70
|
+
f"You said:\n\n> {user_text}\n\n"
|
|
71
|
+
"Configure a real provider (OpenAI key, or your gateway) to get real answers."
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
body = (
|
|
75
|
+
"I'm the offline echo model. Type a message and I'll repeat it back. "
|
|
76
|
+
"Configure a real provider to get real answers."
|
|
77
|
+
)
|
|
78
|
+
in_tokens = _estimate_tokens(user_text)
|
|
79
|
+
out_tokens = _estimate_tokens(body)
|
|
80
|
+
return ModelTurn(
|
|
81
|
+
final_text=body,
|
|
82
|
+
usage={
|
|
83
|
+
"input_tokens": in_tokens,
|
|
84
|
+
"output_tokens": out_tokens,
|
|
85
|
+
"total_tokens": in_tokens + out_tokens,
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def offline_provider_factory(_claims: TokenClaims, config: ModelConfig) -> ModelAdapter:
|
|
91
|
+
"""A ``ProviderAdapterFactory`` for ``LlmGatewayBackend`` serving the offline echo model.
|
|
92
|
+
|
|
93
|
+
Pass this as ``LlmGatewayBackend(provider_adapter_factory=offline_provider_factory)`` (or use
|
|
94
|
+
``monoid llm-gateway serve --provider fake``) for a key-less gateway.
|
|
95
|
+
"""
|
|
96
|
+
return EchoModelAdapter(model=config)
|