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,220 @@
|
|
|
1
|
+
"""OpenTelemetry tracing for monkeybot (optional ``[observability]`` extra).
|
|
2
|
+
|
|
3
|
+
Master switch: ``MONKEYBOT_OTEL_ENABLED`` must be truthy to opt in. When unset,
|
|
4
|
+
tracing stays disabled even if OTel packages are installed (explicit rollout).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from monkeybot.observability import _state
|
|
16
|
+
from monkeybot.observability.instrumentation import (
|
|
17
|
+
ObservingProvider,
|
|
18
|
+
add_tool_hook_span_event,
|
|
19
|
+
get_current_trace_id_hex_optional,
|
|
20
|
+
instrument_fastapi_app,
|
|
21
|
+
)
|
|
22
|
+
from monkeybot.observability.propagation import extract_traceparent, inject_traceparent
|
|
23
|
+
from monkeybot.observability.spans import (
|
|
24
|
+
set_llm_io,
|
|
25
|
+
set_llm_usage,
|
|
26
|
+
set_run_output,
|
|
27
|
+
set_summarize_turns,
|
|
28
|
+
set_turn_io,
|
|
29
|
+
set_turn_prompt_tokens,
|
|
30
|
+
span_llm,
|
|
31
|
+
span_run,
|
|
32
|
+
span_subagent,
|
|
33
|
+
span_summarize,
|
|
34
|
+
span_tool,
|
|
35
|
+
span_turn,
|
|
36
|
+
truncate,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
_TRUTHY = frozenset({"1", "true", "yes", "on"})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _env_truthy(name: str) -> bool:
|
|
45
|
+
raw = os.environ.get(name)
|
|
46
|
+
if raw is None:
|
|
47
|
+
return False
|
|
48
|
+
return raw.strip().lower() in _TRUTHY
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _env_sdk_disabled() -> bool:
|
|
52
|
+
return os.environ.get("OTEL_SDK_DISABLED", "").strip().lower() in _TRUTHY
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_observability_enabled() -> bool:
|
|
56
|
+
return _state.is_observability_enabled()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _create_span_processor(exporter: str) -> Any:
|
|
60
|
+
"""Build a span processor for ``otlp`` or ``console``. Tests may monkeypatch this."""
|
|
61
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
62
|
+
|
|
63
|
+
if exporter == "otlp":
|
|
64
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
65
|
+
|
|
66
|
+
return BatchSpanProcessor(OTLPSpanExporter())
|
|
67
|
+
if exporter == "console":
|
|
68
|
+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
|
|
69
|
+
|
|
70
|
+
return BatchSpanProcessor(ConsoleSpanExporter())
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def init_observability() -> bool:
|
|
75
|
+
if _state._initialized:
|
|
76
|
+
return _state._enabled
|
|
77
|
+
|
|
78
|
+
_state._initialized = True
|
|
79
|
+
_state._enabled = False
|
|
80
|
+
|
|
81
|
+
if not _env_truthy("MONKEYBOT_OTEL_ENABLED"):
|
|
82
|
+
logger.info("observability disabled (MONKEYBOT_OTEL_ENABLED is not set or false)")
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
if _env_sdk_disabled():
|
|
86
|
+
logger.info("observability disabled (OTEL_SDK_DISABLED=true)")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
trace_api = importlib.import_module("opentelemetry.trace")
|
|
91
|
+
resources = importlib.import_module("opentelemetry.sdk.resources")
|
|
92
|
+
trace_sdk = importlib.import_module("opentelemetry.sdk.trace")
|
|
93
|
+
w3c = importlib.import_module("opentelemetry.trace.propagation.tracecontext")
|
|
94
|
+
propagate = importlib.import_module("opentelemetry.propagate")
|
|
95
|
+
except ImportError:
|
|
96
|
+
logger.warning("observability disabled (OpenTelemetry packages not installed)")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
exporter = os.environ.get("OTEL_TRACES_EXPORTER", "").strip().lower()
|
|
100
|
+
if exporter not in {"otlp", "console"}:
|
|
101
|
+
logger.info(
|
|
102
|
+
"observability disabled (OTEL_TRACES_EXPORTER must be otlp or console, got %r)",
|
|
103
|
+
exporter or "<unset>",
|
|
104
|
+
)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
service_name = os.environ.get("OTEL_SERVICE_NAME", "monkeybot")
|
|
108
|
+
attrs: dict[str, str] = {"service.name": service_name}
|
|
109
|
+
try:
|
|
110
|
+
version = importlib.metadata.version("monkeybot")
|
|
111
|
+
except importlib.metadata.PackageNotFoundError:
|
|
112
|
+
version = None
|
|
113
|
+
if version:
|
|
114
|
+
attrs["service.version"] = version
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
processor = _create_span_processor(exporter)
|
|
118
|
+
if processor is None:
|
|
119
|
+
return False
|
|
120
|
+
resource = resources.Resource.create(attrs)
|
|
121
|
+
provider = trace_sdk.TracerProvider(resource=resource)
|
|
122
|
+
provider.add_span_processor(processor)
|
|
123
|
+
trace_api.set_tracer_provider(provider)
|
|
124
|
+
propagate.set_global_textmap(w3c.TraceContextTextMapPropagator())
|
|
125
|
+
_state._enabled = True
|
|
126
|
+
logger.info("observability enabled (exporter=%s, service=%s)", exporter, service_name)
|
|
127
|
+
return True
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
logger.warning("observability init failed: %s", exc)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def shutdown_observability() -> None:
|
|
134
|
+
if not _state._initialized:
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
if _state._enabled:
|
|
138
|
+
trace_api = importlib.import_module("opentelemetry.trace")
|
|
139
|
+
provider = trace_api.get_tracer_provider()
|
|
140
|
+
shutdown = getattr(provider, "shutdown", None)
|
|
141
|
+
if callable(shutdown):
|
|
142
|
+
try:
|
|
143
|
+
shutdown(timeout_millis=5000)
|
|
144
|
+
except TypeError:
|
|
145
|
+
shutdown()
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
logger.warning("observability shutdown failed: %s", exc)
|
|
148
|
+
finally:
|
|
149
|
+
_state._initialized = False
|
|
150
|
+
_state._enabled = False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class _NoOpTracer:
|
|
154
|
+
def start_span(self, *_a: Any, **_k: Any) -> _NoOpSpan:
|
|
155
|
+
return _NoOpSpan()
|
|
156
|
+
|
|
157
|
+
def start_as_current_span(self, *_a: Any, **_k: Any) -> _NoOpSpan:
|
|
158
|
+
return _NoOpSpan()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _NoOpSpan:
|
|
162
|
+
def __enter__(self) -> _NoOpSpan:
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def __exit__(self, *_a: Any) -> None:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def set_attribute(self, *_a: Any, **_k: Any) -> None:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def record_exception(self, *_a: Any, **_k: Any) -> None:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def set_status(self, *_a: Any, **_k: Any) -> None:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def end(self) -> None:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def is_recording(self) -> bool:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def get_tracer(name: str = "monkeybot") -> Any:
|
|
185
|
+
if not _state._enabled:
|
|
186
|
+
try:
|
|
187
|
+
from opentelemetry.trace import NoOpTracer
|
|
188
|
+
|
|
189
|
+
return NoOpTracer()
|
|
190
|
+
except ImportError:
|
|
191
|
+
return _NoOpTracer()
|
|
192
|
+
trace_api = importlib.import_module("opentelemetry.trace")
|
|
193
|
+
return trace_api.get_tracer(name)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"ObservingProvider",
|
|
198
|
+
"add_tool_hook_span_event",
|
|
199
|
+
"extract_traceparent",
|
|
200
|
+
"get_current_trace_id_hex_optional",
|
|
201
|
+
"instrument_fastapi_app",
|
|
202
|
+
"get_tracer",
|
|
203
|
+
"init_observability",
|
|
204
|
+
"inject_traceparent",
|
|
205
|
+
"is_observability_enabled",
|
|
206
|
+
"set_llm_io",
|
|
207
|
+
"set_llm_usage",
|
|
208
|
+
"set_run_output",
|
|
209
|
+
"set_summarize_turns",
|
|
210
|
+
"set_turn_io",
|
|
211
|
+
"set_turn_prompt_tokens",
|
|
212
|
+
"shutdown_observability",
|
|
213
|
+
"span_llm",
|
|
214
|
+
"span_run",
|
|
215
|
+
"span_subagent",
|
|
216
|
+
"span_summarize",
|
|
217
|
+
"span_tool",
|
|
218
|
+
"span_turn",
|
|
219
|
+
"truncate",
|
|
220
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Extended instrumentation: provider wrapper, FastAPI, hook span events, trace id helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import AsyncIterator, Sequence
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
8
|
+
|
|
9
|
+
from monkeybot.core.context import TurnContext
|
|
10
|
+
from monkeybot.core.llm.provider import (
|
|
11
|
+
Message,
|
|
12
|
+
Provider,
|
|
13
|
+
ProviderEvent,
|
|
14
|
+
ToolDef,
|
|
15
|
+
provider_count_input_tokens,
|
|
16
|
+
provider_stream,
|
|
17
|
+
)
|
|
18
|
+
from monkeybot.observability._state import is_observability_enabled
|
|
19
|
+
from monkeybot.observability.spans import span_llm
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from fastapi import FastAPI
|
|
25
|
+
|
|
26
|
+
HookPhase = Literal["pre_tool", "post_tool"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ObservingProvider:
|
|
30
|
+
"""Wraps a :class:`~monkeybot.core.llm.provider.Provider` for direct (non-loop) ``stream`` calls."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, inner: Provider, *, turn_ctx: TurnContext | None = None) -> None:
|
|
33
|
+
self._inner = inner
|
|
34
|
+
self._turn_ctx = turn_ctx
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
return self._inner.name
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def supports_streaming(self) -> bool:
|
|
42
|
+
return self._inner.supports_streaming
|
|
43
|
+
|
|
44
|
+
async def count_input_tokens(
|
|
45
|
+
self,
|
|
46
|
+
messages: Sequence[Message],
|
|
47
|
+
tools: Sequence[ToolDef],
|
|
48
|
+
*,
|
|
49
|
+
model: str,
|
|
50
|
+
thinking_budget: int | None = None,
|
|
51
|
+
vertex_google_search: bool = False,
|
|
52
|
+
) -> int:
|
|
53
|
+
return await provider_count_input_tokens(
|
|
54
|
+
self._inner,
|
|
55
|
+
messages,
|
|
56
|
+
tools,
|
|
57
|
+
model=model,
|
|
58
|
+
thinking_budget=thinking_budget,
|
|
59
|
+
vertex_google_search=vertex_google_search,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def stream(
|
|
63
|
+
self,
|
|
64
|
+
messages: Sequence[Message],
|
|
65
|
+
tools: Sequence[ToolDef],
|
|
66
|
+
*,
|
|
67
|
+
model: str,
|
|
68
|
+
thinking_budget: int | None = None,
|
|
69
|
+
vertex_google_search: bool = False,
|
|
70
|
+
) -> AsyncIterator[ProviderEvent]:
|
|
71
|
+
inner_stream = provider_stream(
|
|
72
|
+
self._inner,
|
|
73
|
+
messages,
|
|
74
|
+
tools,
|
|
75
|
+
model=model,
|
|
76
|
+
thinking_budget=thinking_budget,
|
|
77
|
+
vertex_google_search=vertex_google_search,
|
|
78
|
+
)
|
|
79
|
+
if not is_observability_enabled():
|
|
80
|
+
return inner_stream
|
|
81
|
+
|
|
82
|
+
async def _observed() -> AsyncIterator[ProviderEvent]:
|
|
83
|
+
ctx = self._correlation_ctx(model)
|
|
84
|
+
async with span_llm(
|
|
85
|
+
ctx=ctx,
|
|
86
|
+
model=model,
|
|
87
|
+
vertex_google_search=vertex_google_search,
|
|
88
|
+
):
|
|
89
|
+
async for evt in inner_stream:
|
|
90
|
+
yield evt
|
|
91
|
+
|
|
92
|
+
return _observed()
|
|
93
|
+
|
|
94
|
+
def _correlation_ctx(self, model: str) -> TurnContext:
|
|
95
|
+
if self._turn_ctx is not None:
|
|
96
|
+
return self._turn_ctx
|
|
97
|
+
return TurnContext(
|
|
98
|
+
thread_id="direct",
|
|
99
|
+
request_id="direct",
|
|
100
|
+
agent_md="",
|
|
101
|
+
memory_index=[],
|
|
102
|
+
skills=[],
|
|
103
|
+
tools=[],
|
|
104
|
+
user_id=None,
|
|
105
|
+
parent_run_id=None,
|
|
106
|
+
model=model,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def instrument_fastapi_app(app: object) -> None:
|
|
111
|
+
"""Instrument a FastAPI app with OTel HTTP server spans (idempotent, defensive)."""
|
|
112
|
+
if not is_observability_enabled():
|
|
113
|
+
return
|
|
114
|
+
if getattr(app, "_monkeybot_otel_fastapi_instrumented", False):
|
|
115
|
+
return
|
|
116
|
+
try:
|
|
117
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
|
118
|
+
except ImportError:
|
|
119
|
+
logger.warning(
|
|
120
|
+
"FastAPI OpenTelemetry instrumentation unavailable (install [observability] extra)"
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
FastAPIInstrumentor().instrument_app(cast("FastAPI", app))
|
|
124
|
+
cast(Any, app)._monkeybot_otel_fastapi_instrumented = True
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_current_trace_id_hex_optional() -> str | None:
|
|
128
|
+
"""Return W3C trace id (32 lower-hex chars) for the current span, or ``None``."""
|
|
129
|
+
if not is_observability_enabled():
|
|
130
|
+
return None
|
|
131
|
+
from opentelemetry import trace
|
|
132
|
+
from opentelemetry.trace import format_trace_id
|
|
133
|
+
|
|
134
|
+
span = trace.get_current_span()
|
|
135
|
+
ctx = span.get_span_context()
|
|
136
|
+
if not ctx.is_valid:
|
|
137
|
+
return None
|
|
138
|
+
return format_trace_id(ctx.trace_id)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def add_tool_hook_span_event(*, phase: HookPhase, tool_name: str) -> None:
|
|
142
|
+
"""Record a hook lifecycle event on the active span (``monkeybot.tool`` when in loop)."""
|
|
143
|
+
if not is_observability_enabled():
|
|
144
|
+
return
|
|
145
|
+
from opentelemetry import trace
|
|
146
|
+
|
|
147
|
+
event_name = (
|
|
148
|
+
"monkeybot.hook.pre_tool" if phase == "pre_tool" else "monkeybot.hook.post_tool"
|
|
149
|
+
)
|
|
150
|
+
span = trace.get_current_span()
|
|
151
|
+
if not span.is_recording():
|
|
152
|
+
return
|
|
153
|
+
span.add_event(event_name, {"tool.name": tool_name})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""W3C trace context propagation for subagent subprocesses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from monkeybot.observability._state import is_observability_enabled
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from opentelemetry.context import Context
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def inject_traceparent(carrier: dict[str, str]) -> None:
|
|
17
|
+
"""Inject W3C traceparent (and tracestate if present) from the current context.
|
|
18
|
+
|
|
19
|
+
No-op when observability is disabled, OTel is unavailable, or the current span
|
|
20
|
+
context is invalid. Never raises.
|
|
21
|
+
"""
|
|
22
|
+
if not is_observability_enabled():
|
|
23
|
+
return
|
|
24
|
+
try:
|
|
25
|
+
from opentelemetry import propagate, trace
|
|
26
|
+
except ImportError:
|
|
27
|
+
return
|
|
28
|
+
try:
|
|
29
|
+
span_ctx = trace.get_current_span().get_span_context()
|
|
30
|
+
if not span_ctx.is_valid:
|
|
31
|
+
return
|
|
32
|
+
propagate.inject(carrier)
|
|
33
|
+
except Exception:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def extract_traceparent(carrier: dict[str, str]) -> Context | None:
|
|
38
|
+
"""Extract an OTel Context from carrier keys ``traceparent`` / ``tracestate``.
|
|
39
|
+
|
|
40
|
+
Returns None when the value is missing, malformed, or yields a non-valid span context.
|
|
41
|
+
Logs a WARNING once per bad value (include no user content). Never raises.
|
|
42
|
+
"""
|
|
43
|
+
raw = carrier.get("traceparent")
|
|
44
|
+
if raw is None:
|
|
45
|
+
return None
|
|
46
|
+
if not isinstance(raw, str):
|
|
47
|
+
logger.warning("malformed traceparent in carrier: expected string")
|
|
48
|
+
return None
|
|
49
|
+
traceparent = raw.strip()
|
|
50
|
+
if not traceparent:
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
from opentelemetry import propagate, trace
|
|
54
|
+
except ImportError:
|
|
55
|
+
return None
|
|
56
|
+
try:
|
|
57
|
+
ctx = propagate.extract(carrier)
|
|
58
|
+
span_ctx = trace.get_current_span(ctx).get_span_context()
|
|
59
|
+
if not span_ctx.is_valid:
|
|
60
|
+
logger.warning("malformed traceparent: invalid span context after extract")
|
|
61
|
+
return None
|
|
62
|
+
return ctx
|
|
63
|
+
except Exception:
|
|
64
|
+
logger.warning("malformed traceparent: propagation extract failed")
|
|
65
|
+
return None
|