monkeybot 2.1.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.
- monkeybot/__init__.py +3 -0
- monkeybot/cli/__init__.py +3 -0
- monkeybot/cli/__main__.py +8 -0
- monkeybot/cli/audio_io.py +8 -0
- monkeybot/cli/gateway_manager.py +17 -0
- monkeybot/cli/main.py +22 -0
- monkeybot/cli/push_to_talk.py +12 -0
- monkeybot/cli/realtime_client.py +13 -0
- monkeybot/core/__init__.py +19 -0
- monkeybot/core/attachments/__init__.py +22 -0
- monkeybot/core/attachments/catalog.py +62 -0
- monkeybot/core/attachments/config.py +52 -0
- monkeybot/core/attachments/freeze.py +158 -0
- monkeybot/core/attachments/resolve.py +70 -0
- monkeybot/core/attachments/store.py +180 -0
- monkeybot/core/attachments/text.py +72 -0
- monkeybot/core/attachments/tools.py +54 -0
- monkeybot/core/bootstrap.py +242 -0
- monkeybot/core/config/__init__.py +71 -0
- monkeybot/core/config/realtime_config.py +150 -0
- monkeybot/core/config/runtime_env.py +262 -0
- monkeybot/core/config/settings.py +341 -0
- monkeybot/core/config/validation.py +249 -0
- monkeybot/core/config/yaml_loader.py +45 -0
- monkeybot/core/context/__init__.py +781 -0
- monkeybot/core/context/campaign_context.py +8 -0
- monkeybot/core/context/common.py +14 -0
- monkeybot/core/context/curator.py +255 -0
- monkeybot/core/context/epoch.py +226 -0
- monkeybot/core/context/memory_prompt.py +222 -0
- monkeybot/core/context/tool_output_policy.py +270 -0
- monkeybot/core/context/tool_result_ingress.py +290 -0
- monkeybot/core/context/tool_shapers.py +361 -0
- monkeybot/core/hooks/__init__.py +261 -0
- monkeybot/core/llm/__init__.py +4 -0
- monkeybot/core/llm/provider.py +296 -0
- monkeybot/core/llm/realtime_provider.py +203 -0
- monkeybot/core/llm/usage.py +57 -0
- monkeybot/core/logging_utils.py +24 -0
- monkeybot/core/mcp/__init__.py +1 -0
- monkeybot/core/mcp/mcp_client.py +1215 -0
- monkeybot/core/mcp/ports_mcp.py +109 -0
- monkeybot/core/memory/__init__.py +24 -0
- monkeybot/core/memory/hook.py +413 -0
- monkeybot/core/memory/index_format.py +104 -0
- monkeybot/core/memory/integrity.py +180 -0
- monkeybot/core/memory/organizer.py +270 -0
- monkeybot/core/memory/storage_ops.py +139 -0
- monkeybot/core/memory/subsystem.py +91 -0
- monkeybot/core/messages/__init__.py +16 -0
- monkeybot/core/messages/convert_provider.py +41 -0
- monkeybot/core/messages/tool_integrity.py +262 -0
- monkeybot/core/messages/transform_context.py +84 -0
- monkeybot/core/path_safety.py +11 -0
- monkeybot/core/persistence/__init__.py +17 -0
- monkeybot/core/persistence/backends.py +236 -0
- monkeybot/core/persistence/db.py +28 -0
- monkeybot/core/persistence/durable_runs.py +286 -0
- monkeybot/core/persistence/firestore.py +658 -0
- monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
- monkeybot/core/persistence/history.py +156 -0
- monkeybot/core/persistence/postgres.py +895 -0
- monkeybot/core/persistence/runs.py +76 -0
- monkeybot/core/persistence/scheduled_loops.py +435 -0
- monkeybot/core/persistence/session_turn_locks.py +94 -0
- monkeybot/core/persistence/sqlite.py +218 -0
- monkeybot/core/persistence/sqlite_backend.py +74 -0
- monkeybot/core/persistence/thread_summary.py +61 -0
- monkeybot/core/persistence/transcript.py +194 -0
- monkeybot/core/persistence/usage.py +149 -0
- monkeybot/core/prompts/__init__.py +1 -0
- monkeybot/core/prompts/harness_prompt.py +197 -0
- monkeybot/core/prompts/prompt.py +215 -0
- monkeybot/core/runtime/__init__.py +1 -0
- monkeybot/core/runtime/context_budget.py +267 -0
- monkeybot/core/runtime/events.py +819 -0
- monkeybot/core/runtime/input_admission.py +154 -0
- monkeybot/core/runtime/loop.py +2374 -0
- monkeybot/core/runtime/provider_stream_mapper.py +159 -0
- monkeybot/core/runtime/realtime_loop.py +654 -0
- monkeybot/core/runtime/utterance_buffer.py +179 -0
- monkeybot/core/subagents/__init__.py +1 -0
- monkeybot/core/subagents/subagent_proto.py +331 -0
- monkeybot/core/subagents/subagent_worker.py +441 -0
- monkeybot/core/subagents/worker_pool.py +403 -0
- monkeybot/core/testing/__init__.py +1 -0
- monkeybot/core/testing/mocks_provider.py +86 -0
- monkeybot/core/testing/mocks_realtime_provider.py +137 -0
- monkeybot/core/tools/__init__.py +1 -0
- monkeybot/core/tools/core_tool_executor.py +1548 -0
- monkeybot/core/tools/inspector.py +226 -0
- monkeybot/core/tools/loop_inspector.py +45 -0
- monkeybot/core/tools/patch.py +480 -0
- monkeybot/core/tools/permission.py +284 -0
- monkeybot/core/tools/sandbox_executor.py +255 -0
- monkeybot/core/tools/spill_inventory.py +35 -0
- monkeybot/core/tools/terminal.py +381 -0
- monkeybot/core/tools/text_normalize.py +25 -0
- monkeybot/core/tools/types.py +33 -0
- monkeybot/core/tools/workspace_service.py +710 -0
- monkeybot/core/tools/workspace_tools.py +116 -0
- monkeybot/core/types/__init__.py +1 -0
- monkeybot/core/types/content_blocks.py +644 -0
- monkeybot/core/types/interfaces.py +156 -0
- monkeybot/core/types/types_tools.py +29 -0
- monkeybot/core/workspace/__init__.py +8 -0
- monkeybot/core/workspace/factory.py +45 -0
- monkeybot/core/workspace/gcs.py +130 -0
- monkeybot/core/workspace/local.py +162 -0
- monkeybot/core/workspace/protocol.py +45 -0
- monkeybot/core/workspace/s3.py +151 -0
- monkeybot/core/workspace_layout.py +27 -0
- monkeybot/gateway/__init__.py +1 -0
- monkeybot/gateway/bootstrap.py +18 -0
- monkeybot/gateway/main.py +47 -0
- monkeybot/gateway/realtime/__init__.py +31 -0
- monkeybot/gateway/realtime/app.py +321 -0
- monkeybot/gateway/realtime/deps.py +52 -0
- monkeybot/gateway/realtime/errors.py +81 -0
- monkeybot/gateway/realtime/guardrails.py +88 -0
- monkeybot/gateway/realtime/manager.py +77 -0
- monkeybot/gateway/realtime/metrics.py +144 -0
- monkeybot/gateway/realtime/routes.py +864 -0
- monkeybot/gateway/realtime/session.py +232 -0
- monkeybot/gateway/realtime/wire.py +412 -0
- monkeybot/gateway/realtime_main.py +49 -0
- monkeybot/gateway/sse/__init__.py +1 -0
- monkeybot/gateway/sse/app.py +733 -0
- monkeybot/gateway/sse/loop_port.py +31 -0
- monkeybot/gateway/sse/models.py +177 -0
- monkeybot/gateway/sse/reply_body.py +91 -0
- monkeybot/gateway/sse/routes.py +1101 -0
- monkeybot/gateway/sse/scheduler_routes.py +200 -0
- monkeybot/gateway/sse/scheduler_wiring.py +96 -0
- monkeybot/gateway/sse/session_bus.py +226 -0
- monkeybot/gateway/sse/sse.py +46 -0
- monkeybot/gateway/sse/workspace_layout.py +7 -0
- monkeybot/observability/__init__.py +220 -0
- monkeybot/observability/_state.py +10 -0
- monkeybot/observability/instrumentation.py +153 -0
- monkeybot/observability/propagation.py +65 -0
- monkeybot/observability/spans.py +455 -0
- monkeybot/providers/__init__.py +19 -0
- monkeybot/providers/_openai_compat.py +450 -0
- monkeybot/providers/_utils.py +473 -0
- monkeybot/providers/bedrock.py +145 -0
- monkeybot/providers/claude.py +125 -0
- monkeybot/providers/gemini.py +677 -0
- monkeybot/providers/gemini_live.py +398 -0
- monkeybot/providers/huggingface.py +129 -0
- monkeybot/providers/nvidia.py +104 -0
- monkeybot/providers/ollama.py +152 -0
- monkeybot/providers/openai.py +127 -0
- monkeybot/providers/pricing.py +60 -0
- monkeybot/providers/sampling.py +44 -0
- monkeybot/providers/vertex_claude.py +148 -0
- monkeybot/scaffold/__init__.py +33 -0
- monkeybot/scheduler/__init__.py +13 -0
- monkeybot/scheduler/__main__.py +4 -0
- monkeybot/scheduler/engine.py +333 -0
- monkeybot/scheduler/http_invoker.py +61 -0
- monkeybot/scheduler/interval.py +77 -0
- monkeybot/scheduler/tick_result.py +34 -0
- monkeybot/scheduler/worker.py +87 -0
- monkeybot/subagents/__init__.py +1 -0
- monkeybot/subagents/worker/__init__.py +1 -0
- monkeybot/subagents/worker/__main__.py +22 -0
- monkeybot/web_search/__init__.py +82 -0
- monkeybot/web_search/backends/__init__.py +5 -0
- monkeybot/web_search/backends/duckduckgo.py +32 -0
- monkeybot/web_search/backends/firecrawl.py +43 -0
- monkeybot/web_search/backends/tavily.py +45 -0
- monkeybot/web_search/protocol.py +25 -0
- monkeybot/web_search/tool.py +56 -0
- monkeybot-2.1.1.dist-info/METADATA +318 -0
- monkeybot-2.1.1.dist-info/RECORD +178 -0
- monkeybot-2.1.1.dist-info/WHEEL +4 -0
- monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Vendor-agnostic contract for persistent duplex realtime model sessions.
|
|
2
|
+
|
|
3
|
+
This protocol is parallel to :class:`~monkeybot.core.llm.provider.Provider` but is
|
|
4
|
+
not a variant of it. Realtime vendor APIs (Gemini Live, OpenAI Realtime, Bedrock
|
|
5
|
+
Nova Sonic) are persistent duplex sessions, not request/response-per-call streams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncIterator, Sequence
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Literal, Protocol, TypeAlias
|
|
13
|
+
|
|
14
|
+
from monkeybot.core.types.types_tools import ToolDef
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AudioFormat:
|
|
19
|
+
"""Audio wire format for realtime input/output."""
|
|
20
|
+
|
|
21
|
+
encoding: str
|
|
22
|
+
sample_rate_hz: int
|
|
23
|
+
channels: int
|
|
24
|
+
frame_ms: int
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
return f"{self.encoding}_{self.sample_rate_hz}hz_{self.channels}ch_{self.frame_ms}ms"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class RealtimeSessionConfig:
|
|
32
|
+
"""Parameters used to open a persistent realtime vendor session."""
|
|
33
|
+
|
|
34
|
+
model: str
|
|
35
|
+
system_prompt: str
|
|
36
|
+
tools: Sequence[ToolDef]
|
|
37
|
+
preferred_input_format: AudioFormat = AudioFormat(
|
|
38
|
+
encoding="pcm_s16le", sample_rate_hz=24000, channels=1, frame_ms=200
|
|
39
|
+
)
|
|
40
|
+
preferred_output_format: AudioFormat = AudioFormat(
|
|
41
|
+
encoding="pcm_s16le", sample_rate_hz=24000, channels=1, frame_ms=200
|
|
42
|
+
)
|
|
43
|
+
voice: str | None = None
|
|
44
|
+
max_output_tokens: int | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class RealtimePartialTranscript:
|
|
49
|
+
kind: Literal["RealtimePartialTranscript"] = "RealtimePartialTranscript"
|
|
50
|
+
text: str = ""
|
|
51
|
+
is_final: bool = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class RealtimeAudioDelta:
|
|
56
|
+
kind: Literal["RealtimeAudioDelta"] = "RealtimeAudioDelta"
|
|
57
|
+
chunk: bytes = b""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class RealtimeTextDelta:
|
|
62
|
+
"""Text delta emitted by the model during a realtime turn."""
|
|
63
|
+
|
|
64
|
+
kind: Literal["RealtimeTextDelta"] = "RealtimeTextDelta"
|
|
65
|
+
text: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class RealtimeTurnBoundary:
|
|
70
|
+
"""Provider signaled the end of the current user or assistant turn."""
|
|
71
|
+
|
|
72
|
+
kind: Literal["RealtimeTurnBoundary"] = "RealtimeTurnBoundary"
|
|
73
|
+
role: Literal["user", "assistant"] = "user"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class RealtimeToolCall:
|
|
78
|
+
"""Tool call emitted by a realtime provider at turn boundary."""
|
|
79
|
+
|
|
80
|
+
kind: Literal["RealtimeToolCall"] = "RealtimeToolCall"
|
|
81
|
+
call_id: str = ""
|
|
82
|
+
name: str = ""
|
|
83
|
+
args: dict[str, object] = field(default_factory=dict)
|
|
84
|
+
parse_error: str | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class RealtimeInterrupted:
|
|
89
|
+
"""Provider acknowledged that the current turn was interrupted."""
|
|
90
|
+
|
|
91
|
+
kind: Literal["RealtimeInterrupted"] = "RealtimeInterrupted"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class RealtimeUsage:
|
|
96
|
+
kind: Literal["RealtimeUsage"] = "RealtimeUsage"
|
|
97
|
+
input_tokens: int = 0
|
|
98
|
+
output_tokens: int = 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class RealtimeError:
|
|
103
|
+
kind: Literal["RealtimeError"] = "RealtimeError"
|
|
104
|
+
error: str = ""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True)
|
|
108
|
+
class RealtimeDone:
|
|
109
|
+
"""Provider has finished emitting output for the current turn."""
|
|
110
|
+
|
|
111
|
+
kind: Literal["RealtimeDone"] = "RealtimeDone"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
RealtimeEvent: TypeAlias = (
|
|
115
|
+
RealtimePartialTranscript
|
|
116
|
+
| RealtimeAudioDelta
|
|
117
|
+
| RealtimeTextDelta
|
|
118
|
+
| RealtimeTurnBoundary
|
|
119
|
+
| RealtimeToolCall
|
|
120
|
+
| RealtimeInterrupted
|
|
121
|
+
| RealtimeUsage
|
|
122
|
+
| RealtimeError
|
|
123
|
+
| RealtimeDone
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RealtimeSession(Protocol):
|
|
128
|
+
"""One open realtime vendor session."""
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def input_format(self) -> AudioFormat:
|
|
132
|
+
"""Actual negotiated input format (may differ from requested)."""
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def output_format(self) -> AudioFormat:
|
|
136
|
+
"""Actual negotiated output format (may differ from requested)."""
|
|
137
|
+
|
|
138
|
+
async def send_audio(self, chunk: bytes) -> None:
|
|
139
|
+
"""Send one audio chunk from the user."""
|
|
140
|
+
|
|
141
|
+
async def end_audio_turn(self) -> None:
|
|
142
|
+
"""Signal that the user finished speaking (end of push-to-talk utterance).
|
|
143
|
+
|
|
144
|
+
Providers that use voice-activity detection can treat this as an explicit
|
|
145
|
+
end-of-speech cue so the model responds immediately instead of waiting for
|
|
146
|
+
silence timeout.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
async def send_text(self, text: str) -> None:
|
|
150
|
+
"""Send text from the user."""
|
|
151
|
+
|
|
152
|
+
async def send_context(self, text: str) -> None:
|
|
153
|
+
"""Inject non-user context (e.g., a tool result) into the live session."""
|
|
154
|
+
|
|
155
|
+
async def send_tool_results(
|
|
156
|
+
self,
|
|
157
|
+
results: Sequence[tuple[str, str, dict[str, object], bool]],
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Return tool results to the live session.
|
|
160
|
+
|
|
161
|
+
Each result is ``(call_id, tool_name, response_payload, is_error)``.
|
|
162
|
+
Providers that require a dedicated tool-response RPC (e.g. Gemini Live
|
|
163
|
+
``send_tool_response``) must implement that here. Providers without a
|
|
164
|
+
dedicated RPC may fall back to ``send_context``.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
async def interrupt(self) -> None:
|
|
168
|
+
"""Tell the provider to cancel the current assistant turn."""
|
|
169
|
+
|
|
170
|
+
def events(self) -> AsyncIterator[RealtimeEvent]:
|
|
171
|
+
"""Yield provider events for the lifetime of the session."""
|
|
172
|
+
|
|
173
|
+
async def close(self, *, reason: str = "session_end") -> None:
|
|
174
|
+
"""Close the vendor session gracefully."""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class RealtimeProvider(Protocol):
|
|
178
|
+
"""Factory for persistent duplex realtime sessions."""
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def name(self) -> str:
|
|
182
|
+
"""Stable provider id (e.g. ``"gemini-live"``)."""
|
|
183
|
+
|
|
184
|
+
async def connect(self, *, config: RealtimeSessionConfig) -> RealtimeSession:
|
|
185
|
+
"""Open a new vendor session and return it."""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
__all__ = [
|
|
189
|
+
"AudioFormat",
|
|
190
|
+
"RealtimeDone",
|
|
191
|
+
"RealtimeError",
|
|
192
|
+
"RealtimeEvent",
|
|
193
|
+
"RealtimeInterrupted",
|
|
194
|
+
"RealtimePartialTranscript",
|
|
195
|
+
"RealtimeProvider",
|
|
196
|
+
"RealtimeSession",
|
|
197
|
+
"RealtimeSessionConfig",
|
|
198
|
+
"RealtimeToolCall",
|
|
199
|
+
"RealtimeTextDelta",
|
|
200
|
+
"RealtimeTurnBoundary",
|
|
201
|
+
"RealtimeUsage",
|
|
202
|
+
"RealtimeAudioDelta",
|
|
203
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Per-turn token and cost data types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from monkeybot.core.runtime.events import UsageTotals
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Usage:
|
|
12
|
+
"""Aggregated usage for one model turn."""
|
|
13
|
+
|
|
14
|
+
input_tokens: int = 0
|
|
15
|
+
output_tokens: int = 0
|
|
16
|
+
cached_tokens: int = 0
|
|
17
|
+
cache_read_tokens: int = 0
|
|
18
|
+
cache_creation_tokens: int = 0
|
|
19
|
+
cost_usd: float = 0.0
|
|
20
|
+
duration_ms: int = 0
|
|
21
|
+
estimated_prompt_tokens: int = 0
|
|
22
|
+
"""Peak pre-stream input token count for this user turn (``Provider.count_input_tokens``).
|
|
23
|
+
|
|
24
|
+
Same payload as each outbound ``stream`` call (messages + tools), using the
|
|
25
|
+
vendor count API or tokenizer where implemented.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def usage_from_totals(t: UsageTotals) -> Usage:
|
|
30
|
+
"""Build a :class:`Usage` row from :class:`~monkeybot.core.runtime.events.UsageTotals` (e.g. ``TurnComplete.usage``)."""
|
|
31
|
+
return Usage(
|
|
32
|
+
input_tokens=t.input_tokens,
|
|
33
|
+
output_tokens=t.output_tokens,
|
|
34
|
+
cached_tokens=t.cached_tokens,
|
|
35
|
+
cache_read_tokens=t.cache_read_tokens,
|
|
36
|
+
cache_creation_tokens=t.cache_creation_tokens,
|
|
37
|
+
cost_usd=t.cost_usd,
|
|
38
|
+
duration_ms=t.duration_ms,
|
|
39
|
+
estimated_prompt_tokens=t.estimated_prompt_tokens,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class UsageSummary:
|
|
45
|
+
"""Aggregated totals returned by a usage store's ``summary`` method."""
|
|
46
|
+
|
|
47
|
+
turns: int
|
|
48
|
+
input_tokens: int
|
|
49
|
+
output_tokens: int
|
|
50
|
+
cached_tokens: int
|
|
51
|
+
cost_usd: float
|
|
52
|
+
period_start_ms: int | None
|
|
53
|
+
period_end_ms: int | None
|
|
54
|
+
last_prompt_tokens: int
|
|
55
|
+
last_estimated_prompt_tokens: int
|
|
56
|
+
cache_read_tokens: int = 0
|
|
57
|
+
cache_creation_tokens: int = 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Shared logging helpers for gateway, workers, and deploy entrypoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_log_level(raw: str | None, *, default: str = "INFO") -> int:
|
|
9
|
+
"""Map ``LOG_LEVEL`` env strings to :mod:`logging` level ints (case-insensitive)."""
|
|
10
|
+
name = (raw or default).strip().upper()
|
|
11
|
+
level = logging.getLevelName(name)
|
|
12
|
+
if isinstance(level, int):
|
|
13
|
+
return level
|
|
14
|
+
raise ValueError(f"invalid log level: {raw!r}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def kv(**fields: object) -> str:
|
|
18
|
+
"""Format ``key=value`` pairs for structured log messages; skip ``None`` values."""
|
|
19
|
+
parts: list[str] = []
|
|
20
|
+
for key, value in fields.items():
|
|
21
|
+
if value is None:
|
|
22
|
+
continue
|
|
23
|
+
parts.append(f"{key}={value}")
|
|
24
|
+
return " ".join(parts)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Model Context Protocol client and port definitions."""
|