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.
Files changed (178) hide show
  1. monkeybot/__init__.py +3 -0
  2. monkeybot/cli/__init__.py +3 -0
  3. monkeybot/cli/__main__.py +8 -0
  4. monkeybot/cli/audio_io.py +8 -0
  5. monkeybot/cli/gateway_manager.py +17 -0
  6. monkeybot/cli/main.py +22 -0
  7. monkeybot/cli/push_to_talk.py +12 -0
  8. monkeybot/cli/realtime_client.py +13 -0
  9. monkeybot/core/__init__.py +19 -0
  10. monkeybot/core/attachments/__init__.py +22 -0
  11. monkeybot/core/attachments/catalog.py +62 -0
  12. monkeybot/core/attachments/config.py +52 -0
  13. monkeybot/core/attachments/freeze.py +158 -0
  14. monkeybot/core/attachments/resolve.py +70 -0
  15. monkeybot/core/attachments/store.py +180 -0
  16. monkeybot/core/attachments/text.py +72 -0
  17. monkeybot/core/attachments/tools.py +54 -0
  18. monkeybot/core/bootstrap.py +242 -0
  19. monkeybot/core/config/__init__.py +71 -0
  20. monkeybot/core/config/realtime_config.py +150 -0
  21. monkeybot/core/config/runtime_env.py +262 -0
  22. monkeybot/core/config/settings.py +341 -0
  23. monkeybot/core/config/validation.py +249 -0
  24. monkeybot/core/config/yaml_loader.py +45 -0
  25. monkeybot/core/context/__init__.py +781 -0
  26. monkeybot/core/context/campaign_context.py +8 -0
  27. monkeybot/core/context/common.py +14 -0
  28. monkeybot/core/context/curator.py +255 -0
  29. monkeybot/core/context/epoch.py +226 -0
  30. monkeybot/core/context/memory_prompt.py +222 -0
  31. monkeybot/core/context/tool_output_policy.py +270 -0
  32. monkeybot/core/context/tool_result_ingress.py +290 -0
  33. monkeybot/core/context/tool_shapers.py +361 -0
  34. monkeybot/core/hooks/__init__.py +261 -0
  35. monkeybot/core/llm/__init__.py +4 -0
  36. monkeybot/core/llm/provider.py +296 -0
  37. monkeybot/core/llm/realtime_provider.py +203 -0
  38. monkeybot/core/llm/usage.py +57 -0
  39. monkeybot/core/logging_utils.py +24 -0
  40. monkeybot/core/mcp/__init__.py +1 -0
  41. monkeybot/core/mcp/mcp_client.py +1215 -0
  42. monkeybot/core/mcp/ports_mcp.py +109 -0
  43. monkeybot/core/memory/__init__.py +24 -0
  44. monkeybot/core/memory/hook.py +413 -0
  45. monkeybot/core/memory/index_format.py +104 -0
  46. monkeybot/core/memory/integrity.py +180 -0
  47. monkeybot/core/memory/organizer.py +270 -0
  48. monkeybot/core/memory/storage_ops.py +139 -0
  49. monkeybot/core/memory/subsystem.py +91 -0
  50. monkeybot/core/messages/__init__.py +16 -0
  51. monkeybot/core/messages/convert_provider.py +41 -0
  52. monkeybot/core/messages/tool_integrity.py +262 -0
  53. monkeybot/core/messages/transform_context.py +84 -0
  54. monkeybot/core/path_safety.py +11 -0
  55. monkeybot/core/persistence/__init__.py +17 -0
  56. monkeybot/core/persistence/backends.py +236 -0
  57. monkeybot/core/persistence/db.py +28 -0
  58. monkeybot/core/persistence/durable_runs.py +286 -0
  59. monkeybot/core/persistence/firestore.py +658 -0
  60. monkeybot/core/persistence/firestore_scheduled_loops.py +336 -0
  61. monkeybot/core/persistence/history.py +156 -0
  62. monkeybot/core/persistence/postgres.py +895 -0
  63. monkeybot/core/persistence/runs.py +76 -0
  64. monkeybot/core/persistence/scheduled_loops.py +435 -0
  65. monkeybot/core/persistence/session_turn_locks.py +94 -0
  66. monkeybot/core/persistence/sqlite.py +218 -0
  67. monkeybot/core/persistence/sqlite_backend.py +74 -0
  68. monkeybot/core/persistence/thread_summary.py +61 -0
  69. monkeybot/core/persistence/transcript.py +194 -0
  70. monkeybot/core/persistence/usage.py +149 -0
  71. monkeybot/core/prompts/__init__.py +1 -0
  72. monkeybot/core/prompts/harness_prompt.py +197 -0
  73. monkeybot/core/prompts/prompt.py +215 -0
  74. monkeybot/core/runtime/__init__.py +1 -0
  75. monkeybot/core/runtime/context_budget.py +267 -0
  76. monkeybot/core/runtime/events.py +819 -0
  77. monkeybot/core/runtime/input_admission.py +154 -0
  78. monkeybot/core/runtime/loop.py +2374 -0
  79. monkeybot/core/runtime/provider_stream_mapper.py +159 -0
  80. monkeybot/core/runtime/realtime_loop.py +654 -0
  81. monkeybot/core/runtime/utterance_buffer.py +179 -0
  82. monkeybot/core/subagents/__init__.py +1 -0
  83. monkeybot/core/subagents/subagent_proto.py +331 -0
  84. monkeybot/core/subagents/subagent_worker.py +441 -0
  85. monkeybot/core/subagents/worker_pool.py +403 -0
  86. monkeybot/core/testing/__init__.py +1 -0
  87. monkeybot/core/testing/mocks_provider.py +86 -0
  88. monkeybot/core/testing/mocks_realtime_provider.py +137 -0
  89. monkeybot/core/tools/__init__.py +1 -0
  90. monkeybot/core/tools/core_tool_executor.py +1548 -0
  91. monkeybot/core/tools/inspector.py +226 -0
  92. monkeybot/core/tools/loop_inspector.py +45 -0
  93. monkeybot/core/tools/patch.py +480 -0
  94. monkeybot/core/tools/permission.py +284 -0
  95. monkeybot/core/tools/sandbox_executor.py +255 -0
  96. monkeybot/core/tools/spill_inventory.py +35 -0
  97. monkeybot/core/tools/terminal.py +381 -0
  98. monkeybot/core/tools/text_normalize.py +25 -0
  99. monkeybot/core/tools/types.py +33 -0
  100. monkeybot/core/tools/workspace_service.py +710 -0
  101. monkeybot/core/tools/workspace_tools.py +116 -0
  102. monkeybot/core/types/__init__.py +1 -0
  103. monkeybot/core/types/content_blocks.py +644 -0
  104. monkeybot/core/types/interfaces.py +156 -0
  105. monkeybot/core/types/types_tools.py +29 -0
  106. monkeybot/core/workspace/__init__.py +8 -0
  107. monkeybot/core/workspace/factory.py +45 -0
  108. monkeybot/core/workspace/gcs.py +130 -0
  109. monkeybot/core/workspace/local.py +162 -0
  110. monkeybot/core/workspace/protocol.py +45 -0
  111. monkeybot/core/workspace/s3.py +151 -0
  112. monkeybot/core/workspace_layout.py +27 -0
  113. monkeybot/gateway/__init__.py +1 -0
  114. monkeybot/gateway/bootstrap.py +18 -0
  115. monkeybot/gateway/main.py +47 -0
  116. monkeybot/gateway/realtime/__init__.py +31 -0
  117. monkeybot/gateway/realtime/app.py +321 -0
  118. monkeybot/gateway/realtime/deps.py +52 -0
  119. monkeybot/gateway/realtime/errors.py +81 -0
  120. monkeybot/gateway/realtime/guardrails.py +88 -0
  121. monkeybot/gateway/realtime/manager.py +77 -0
  122. monkeybot/gateway/realtime/metrics.py +144 -0
  123. monkeybot/gateway/realtime/routes.py +864 -0
  124. monkeybot/gateway/realtime/session.py +232 -0
  125. monkeybot/gateway/realtime/wire.py +412 -0
  126. monkeybot/gateway/realtime_main.py +49 -0
  127. monkeybot/gateway/sse/__init__.py +1 -0
  128. monkeybot/gateway/sse/app.py +733 -0
  129. monkeybot/gateway/sse/loop_port.py +31 -0
  130. monkeybot/gateway/sse/models.py +177 -0
  131. monkeybot/gateway/sse/reply_body.py +91 -0
  132. monkeybot/gateway/sse/routes.py +1101 -0
  133. monkeybot/gateway/sse/scheduler_routes.py +200 -0
  134. monkeybot/gateway/sse/scheduler_wiring.py +96 -0
  135. monkeybot/gateway/sse/session_bus.py +226 -0
  136. monkeybot/gateway/sse/sse.py +46 -0
  137. monkeybot/gateway/sse/workspace_layout.py +7 -0
  138. monkeybot/observability/__init__.py +220 -0
  139. monkeybot/observability/_state.py +10 -0
  140. monkeybot/observability/instrumentation.py +153 -0
  141. monkeybot/observability/propagation.py +65 -0
  142. monkeybot/observability/spans.py +455 -0
  143. monkeybot/providers/__init__.py +19 -0
  144. monkeybot/providers/_openai_compat.py +450 -0
  145. monkeybot/providers/_utils.py +473 -0
  146. monkeybot/providers/bedrock.py +145 -0
  147. monkeybot/providers/claude.py +125 -0
  148. monkeybot/providers/gemini.py +677 -0
  149. monkeybot/providers/gemini_live.py +398 -0
  150. monkeybot/providers/huggingface.py +129 -0
  151. monkeybot/providers/nvidia.py +104 -0
  152. monkeybot/providers/ollama.py +152 -0
  153. monkeybot/providers/openai.py +127 -0
  154. monkeybot/providers/pricing.py +60 -0
  155. monkeybot/providers/sampling.py +44 -0
  156. monkeybot/providers/vertex_claude.py +148 -0
  157. monkeybot/scaffold/__init__.py +33 -0
  158. monkeybot/scheduler/__init__.py +13 -0
  159. monkeybot/scheduler/__main__.py +4 -0
  160. monkeybot/scheduler/engine.py +333 -0
  161. monkeybot/scheduler/http_invoker.py +61 -0
  162. monkeybot/scheduler/interval.py +77 -0
  163. monkeybot/scheduler/tick_result.py +34 -0
  164. monkeybot/scheduler/worker.py +87 -0
  165. monkeybot/subagents/__init__.py +1 -0
  166. monkeybot/subagents/worker/__init__.py +1 -0
  167. monkeybot/subagents/worker/__main__.py +22 -0
  168. monkeybot/web_search/__init__.py +82 -0
  169. monkeybot/web_search/backends/__init__.py +5 -0
  170. monkeybot/web_search/backends/duckduckgo.py +32 -0
  171. monkeybot/web_search/backends/firecrawl.py +43 -0
  172. monkeybot/web_search/backends/tavily.py +45 -0
  173. monkeybot/web_search/protocol.py +25 -0
  174. monkeybot/web_search/tool.py +56 -0
  175. monkeybot-2.1.1.dist-info/METADATA +318 -0
  176. monkeybot-2.1.1.dist-info/RECORD +178 -0
  177. monkeybot-2.1.1.dist-info/WHEEL +4 -0
  178. monkeybot-2.1.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,151 @@
1
+ """Amazon S3 :class:`WorkspaceStorage` (``boto3``)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+
8
+ from botocore.exceptions import ClientError
9
+
10
+ from monkeybot.core.workspace.protocol import WorkspaceStorage
11
+
12
+ _log = logging.getLogger(__name__)
13
+
14
+
15
+ class S3WorkspaceStorage:
16
+ """Object-prefix layout: ``s3://bucket/{prefix}/{path}``."""
17
+
18
+ def __init__(self, bucket: str, prefix: str = "") -> None:
19
+ import boto3
20
+
21
+ self._s3 = boto3.client("s3")
22
+ self._bucket = bucket
23
+ self._prefix = prefix.strip().strip("/").replace("\\", "/")
24
+ if self._prefix:
25
+ self._prefix = self._prefix + "/"
26
+
27
+ def _key(self, path: str) -> str:
28
+ key = path.strip().replace("\\", "/").lstrip("/")
29
+ return self._prefix + key if self._prefix else key
30
+
31
+ async def read_text(self, path: str) -> str:
32
+ key = self._key(path)
33
+
34
+ def _read() -> str:
35
+ resp = self._s3.get_object(Bucket=self._bucket, Key=key)
36
+ body: bytes = resp["Body"].read()
37
+ return body.decode("utf-8")
38
+
39
+ try:
40
+ return await asyncio.to_thread(_read)
41
+ except ClientError as exc:
42
+ code = exc.response.get("Error", {}).get("Code", "")
43
+ if code in ("404", "NoSuchKey", "NotFound"):
44
+ raise FileNotFoundError(key) from exc
45
+ raise
46
+
47
+ async def write_text(self, path: str, content: str) -> None:
48
+ key = self._key(path)
49
+
50
+ def _write() -> None:
51
+ self._s3.put_object(
52
+ Bucket=self._bucket,
53
+ Key=key,
54
+ Body=content.encode("utf-8"),
55
+ ContentType="text/plain; charset=utf-8",
56
+ )
57
+
58
+ await asyncio.to_thread(_write)
59
+
60
+ async def append_text(self, path: str, content: str) -> None:
61
+ key = self._key(path)
62
+
63
+ def _append() -> None:
64
+ try:
65
+ resp = self._s3.get_object(Bucket=self._bucket, Key=key)
66
+ cur = resp["Body"].read().decode("utf-8")
67
+ except ClientError as exc:
68
+ code = exc.response.get("Error", {}).get("Code", "")
69
+ if code not in ("404", "NoSuchKey", "NotFound"):
70
+ raise
71
+ cur = ""
72
+ self._s3.put_object(
73
+ Bucket=self._bucket,
74
+ Key=key,
75
+ Body=(cur + content).encode("utf-8"),
76
+ ContentType="text/plain; charset=utf-8",
77
+ )
78
+
79
+ await asyncio.to_thread(_append)
80
+
81
+ async def exists(self, path: str) -> bool:
82
+ key = self._key(path)
83
+
84
+ def _head() -> bool:
85
+ try:
86
+ self._s3.head_object(Bucket=self._bucket, Key=key)
87
+ return True
88
+ except ClientError as exc:
89
+ code = exc.response.get("Error", {}).get("Code", "")
90
+ if code in ("404", "NoSuchKey", "NotFound"):
91
+ return False
92
+ raise
93
+
94
+ return await asyncio.to_thread(_head)
95
+
96
+ async def list_files(self, prefix: str = "") -> list[str]:
97
+ pfx = self._key(prefix)
98
+
99
+ def _list() -> list[str]:
100
+ out: list[str] = []
101
+ paginator = self._s3.get_paginator("list_objects_v2")
102
+ for page in paginator.paginate(Bucket=self._bucket, Prefix=pfx):
103
+ for obj in page.get("Contents", []):
104
+ name = obj["Key"]
105
+ if name.endswith("/"):
106
+ continue
107
+ rel = name[len(self._prefix) :] if self._prefix else name
108
+ out.append(rel.lstrip("/"))
109
+ return sorted(out)
110
+
111
+ return await asyncio.to_thread(_list)
112
+
113
+ async def delete(self, path: str) -> None:
114
+ key = self._key(path)
115
+
116
+ def _del() -> None:
117
+ self._s3.delete_object(Bucket=self._bucket, Key=key)
118
+
119
+ try:
120
+ await asyncio.to_thread(_del)
121
+ except ClientError as exc:
122
+ code = exc.response.get("Error", {}).get("Code", "")
123
+ if code not in ("404", "NoSuchKey", "NotFound"):
124
+ raise
125
+
126
+ async def move(self, src: str, dest: str) -> None:
127
+ sk = self._key(src)
128
+ dk = self._key(dest)
129
+
130
+ def _copy_delete() -> None:
131
+ self._s3.copy_object(
132
+ Bucket=self._bucket,
133
+ CopySource={"Bucket": self._bucket, "Key": sk},
134
+ Key=dk,
135
+ )
136
+ try:
137
+ self._s3.delete_object(Bucket=self._bucket, Key=sk)
138
+ except Exception as exc:
139
+ _log.warning("s3 move: delete source failed after copy src=%s dest=%s: %r", sk, dk, exc)
140
+
141
+ await asyncio.to_thread(_copy_delete)
142
+
143
+ async def gc_prefix(self, prefix: str, max_age_sec: float) -> dict[str, int]:
144
+ del prefix, max_age_sec
145
+ _log.info(
146
+ "s3 gc_prefix: skipped — configure S3 lifecycle rules on the bucket for prefix cleanup"
147
+ )
148
+ return {"scanned": 0, "deleted": 0, "errors": 0}
149
+
150
+
151
+ __all__ = ["S3WorkspaceStorage"]
@@ -0,0 +1,27 @@
1
+ """Agent-facing workspace root resolution (shared by gateway and library entry points)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def resolve_agent_workspace_root() -> Path:
10
+ """Directory the agent should treat as its workspace (``data/``, ``skills/``, spill, …).
11
+
12
+ Resolution order:
13
+
14
+ 1. ``MONKEYBOT_WORKSPACE_ROOT`` — absolute path, or relative to process cwd.
15
+ 2. ``WORKSPACE_ROOT`` — same semantics (alias for serverless / legacy handlers).
16
+ 3. Else if ``<cwd>/workspace`` exists and is a directory, use it.
17
+ 4. Else process cwd.
18
+ """
19
+ for env_name in ("MONKEYBOT_WORKSPACE_ROOT", "WORKSPACE_ROOT"):
20
+ raw = os.environ.get(env_name, "").strip()
21
+ if raw:
22
+ p = Path(raw)
23
+ return p.resolve() if p.is_absolute() else (Path.cwd() / p).resolve()
24
+ nested = (Path.cwd() / "workspace").resolve()
25
+ if nested.is_dir():
26
+ return nested
27
+ return Path.cwd().resolve()
@@ -0,0 +1 @@
1
+ """Gateway — FastAPI SSE interface (v2)."""
@@ -0,0 +1,18 @@
1
+ """Process-level bootstrap before the FastAPI app is constructed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from dotenv import load_dotenv
9
+
10
+
11
+ def ensure_gateway_runtime_env() -> None:
12
+ """Load ``.env`` then optional ``monkeybot_config/monkeybot.yaml`` (see ``monkeybot.core.config.runtime_env``)."""
13
+ load_dotenv()
14
+ if not os.environ.get("MONKEYBOT_AGENT_ROOT", "").strip():
15
+ os.environ["MONKEYBOT_AGENT_ROOT"] = str(Path.cwd().resolve())
16
+ from monkeybot.core.config.runtime_env import apply_monkeybot_runtime_env
17
+
18
+ apply_monkeybot_runtime_env()
@@ -0,0 +1,47 @@
1
+ """
2
+ Gateway server entrypoint (SSE harness).
3
+
4
+ Run locally with:
5
+
6
+ python -m monkeybot.gateway.main
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+
14
+ from monkeybot.core.logging_utils import normalize_log_level
15
+ from monkeybot.gateway.bootstrap import ensure_gateway_runtime_env
16
+
17
+ ensure_gateway_runtime_env()
18
+
19
+ logging.basicConfig(
20
+ level=normalize_log_level(os.getenv("LOG_LEVEL")),
21
+ format="%(levelname)s:%(name)s:%(message)s",
22
+ )
23
+ # Suppress chatty third-party loggers regardless of LOG_LEVEL.
24
+ for _noisy in ("httpx", "httpcore", "urllib3", "google_genai", "google.auth"):
25
+ logging.getLogger(_noisy).setLevel(logging.WARNING)
26
+
27
+ from monkeybot.gateway.sse.app import app # noqa: E402
28
+
29
+ __all__ = ["app"]
30
+
31
+
32
+ if __name__ == "__main__":
33
+ import uvicorn
34
+
35
+ port = int(os.getenv("PORT", os.getenv("GATEWAY_PORT", "8000")))
36
+ log_level = os.getenv("LOG_LEVEL", "info").lower()
37
+
38
+ timeout_graceful_shutdown = int(os.getenv("GRACEFUL_SHUTDOWN_TIMEOUT_SEC", "5"))
39
+
40
+ uvicorn.run(
41
+ app,
42
+ host="0.0.0.0",
43
+ port=port,
44
+ log_level=log_level,
45
+ access_log=True,
46
+ timeout_graceful_shutdown=timeout_graceful_shutdown,
47
+ )
@@ -0,0 +1,31 @@
1
+ """Realtime WebSocket gateway for MonkeyBot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from monkeybot.gateway.realtime.app import create_realtime_app
6
+ from monkeybot.gateway.realtime.deps import RealtimeDependencies
7
+ from monkeybot.gateway.realtime.errors import RealtimeError
8
+ from monkeybot.gateway.realtime.guardrails import run_guardrails
9
+ from monkeybot.gateway.realtime.manager import RealtimeSessionManager
10
+ from monkeybot.gateway.realtime.metrics import RealtimeMetrics
11
+ from monkeybot.gateway.realtime.routes import create_realtime_router
12
+ from monkeybot.gateway.realtime.session import RealtimeConnectionState
13
+ from monkeybot.gateway.realtime.wire import (
14
+ ServerConnectedFrame,
15
+ encode_server_frame,
16
+ parse_client_frame,
17
+ )
18
+
19
+ __all__ = [
20
+ "RealtimeConnectionState",
21
+ "RealtimeDependencies",
22
+ "RealtimeError",
23
+ "RealtimeMetrics",
24
+ "RealtimeSessionManager",
25
+ "ServerConnectedFrame",
26
+ "create_realtime_app",
27
+ "create_realtime_router",
28
+ "encode_server_frame",
29
+ "parse_client_frame",
30
+ "run_guardrails",
31
+ ]
@@ -0,0 +1,321 @@
1
+ """FastAPI application factory for the realtime gateway.
2
+
3
+ This creates a new FastAPI app that mounts the SSE gateway routes **plus** the realtime
4
+ WebSocket endpoint. The existing ``gateway/sse/app.py`` is left untouched.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import logging
11
+ import os
12
+ from collections.abc import AsyncIterator
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from fastapi import FastAPI
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+
19
+ from monkeybot.core.attachments.config import attachments_enabled_from_env
20
+ from monkeybot.core.attachments.store import FilesystemAttachmentStore
21
+ from monkeybot.core.config.realtime_config import RealtimeConfig, get_realtime_config
22
+ from monkeybot.core.config.settings import (
23
+ auto_schema_enabled_from_config,
24
+ get_subagent_registry,
25
+ normalize_model_provider,
26
+ vertex_google_search_enabled_from_config,
27
+ )
28
+ from monkeybot.core.hooks import HookManager
29
+ from monkeybot.core.llm.provider import Provider
30
+ from monkeybot.core.mcp.mcp_client import MCPClient
31
+ from monkeybot.core.memory.subsystem import MemorySubsystem
32
+ from monkeybot.core.persistence.backends import create_storage_backend
33
+ from monkeybot.core.tools.inspector import CommandTierInspector, RulesInspector
34
+ from monkeybot.core.tools.permission import try_load_permission_inspector
35
+ from monkeybot.core.workspace import create_workspace_storage
36
+ from monkeybot.gateway.bootstrap import ensure_gateway_runtime_env
37
+ from monkeybot.gateway.sse.routes import create_app as build_sse_app
38
+ from monkeybot.gateway.sse.workspace_layout import resolve_agent_workspace_root
39
+ from monkeybot.providers.gemini_live import GeminiLiveProvider
40
+ from monkeybot.web_search import WebSearchTool, build_backend
41
+
42
+ from .deps import RealtimeDependencies
43
+ from .manager import RealtimeSessionManager
44
+ from .routes import create_realtime_router
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ def _memory_enabled() -> bool:
50
+ raw = os.environ.get("MONKEYBOT_MEMORY_HOOK_ENABLED", "true").strip().lower()
51
+ return raw not in {"0", "false", "no", "off"}
52
+
53
+
54
+ def _memory_storage_uri() -> str:
55
+ uri = os.environ.get("MEMORY_STORAGE_URI", "").strip()
56
+ if uri:
57
+ return uri
58
+ raw_mp = os.environ.get("MEMORY_PATH")
59
+ if raw_mp is not None:
60
+ logger.info(
61
+ "memory: deriving storage URI from MEMORY_PATH; prefer paths.memory_storage_uri in monkeybot.yaml"
62
+ )
63
+ legacy = (raw_mp or "data/memory").strip()
64
+ mem = Path(legacy)
65
+ resolved = mem.resolve() if mem.is_absolute() else (Path.cwd().resolve() / mem).resolve()
66
+ return f"local://{resolved}"
67
+
68
+
69
+ def _tool_denied_patterns() -> list[str]:
70
+ raw = os.environ.get("MONKEYBOT_TOOL_DENIED_PATTERNS")
71
+ if raw is None:
72
+ return ["rm -rf", "/etc/passwd", "DROP TABLE"]
73
+ return [p.strip() for p in raw.split(",") if p.strip()]
74
+
75
+
76
+ def _resolve_provider() -> Provider:
77
+ from monkeybot.core.config.settings import get_provider_config
78
+ from monkeybot.core.testing.mocks_provider import ScriptedFakeProvider
79
+
80
+ mode = normalize_model_provider(os.environ.get("MODEL_PROVIDER", "google_vertexai"))
81
+ if mode == "fake":
82
+ return ScriptedFakeProvider([])
83
+ return get_provider_config(provider=mode).provider
84
+
85
+
86
+ @contextlib.asynccontextmanager
87
+ async def _realtime_lifespan(
88
+ app: FastAPI,
89
+ deps: RealtimeDependencies,
90
+ config: RealtimeConfig,
91
+ ) -> AsyncIterator[None]:
92
+ """Wire storage, MCP, inspectors, memory, and realtime provider."""
93
+ db_url = os.environ.get("DB_URL", "sqlite:///data/monkeybot.db")
94
+ backend = create_storage_backend(db_url)
95
+ await backend.open(run_schema=auto_schema_enabled_from_config())
96
+ app.state.storage = backend
97
+ deps.storage = backend
98
+
99
+ mcp = MCPClient()
100
+ deps.mcp = mcp
101
+ mcp_config = Path(os.environ.get("MCP_CONFIG", "/app/monkeybot_config/mcp.json"))
102
+ strict = os.environ.get("MCP_STRICT_LOAD", "").strip().lower() in ("1", "true", "yes")
103
+ try:
104
+ await mcp.load_from_config(mcp_config, raise_on_error=strict)
105
+ except OSError as exc:
106
+ logger.info("MCP config skipped (%s): %s", mcp_config, exc)
107
+
108
+ tiers_path = Path(
109
+ os.environ.get("COMMAND_ALLOWLIST_CONFIG", "/app/monkeybot_config/command_allowlist.yaml")
110
+ )
111
+ deps.run_command_allowed_commands = None
112
+ deps.run_command_allowed_path_prefixes = None
113
+ inspectors: list[Any] = []
114
+ try:
115
+ tier_insp = CommandTierInspector(tiers_path)
116
+ inspectors.append(tier_insp)
117
+ deps.run_command_allowed_commands = list(tier_insp.allowed_commands)
118
+ deps.run_command_allowed_path_prefixes = list(tier_insp.allowed_path_prefixes)
119
+ except FileNotFoundError:
120
+ logger.info("command tiers missing (%s); allowing all tool calls", tiers_path)
121
+ except Exception as exc:
122
+ logger.exception("command tier load failed: %s", exc)
123
+
124
+ denied = _tool_denied_patterns()
125
+ if denied:
126
+ inspectors.append(RulesInspector(denied))
127
+
128
+ perm_path = Path(
129
+ os.environ.get("PERMISSION_CONFIG", "/app/monkeybot_config/permissions.yaml")
130
+ )
131
+ perm_insp = try_load_permission_inspector(perm_path)
132
+ if perm_insp is not None:
133
+ inspectors.append(perm_insp)
134
+
135
+ deps.inspectors = inspectors
136
+
137
+ # Realtime model can be a Live-only preview while the main turn-based model
138
+ # stays a regular generateContent model. Use realtime.model.provider if set,
139
+ # otherwise fall back to the main provider.
140
+ realtime_provider = normalize_model_provider(
141
+ config.model.provider or os.environ.get("MODEL_PROVIDER", "google_vertexai")
142
+ )
143
+ if realtime_provider == "google_genai":
144
+ api_key = os.environ.get("GEMINI_API_KEY", "")
145
+ if not api_key:
146
+ logger.warning("realtime MODEL_PROVIDER=google_genai but GEMINI_API_KEY is not set")
147
+ deps.realtime_provider = GeminiLiveProvider(api_key=api_key)
148
+ else:
149
+ deps.realtime_provider = GeminiLiveProvider()
150
+
151
+ provider = _resolve_provider()
152
+ vertex_gs = vertex_google_search_enabled_from_config()
153
+ try:
154
+ backend_ws = build_backend()
155
+ if backend_ws is not None:
156
+ deps.web_search_tool = WebSearchTool(backend_ws)
157
+ logger.info("web search enabled: backend=%s", backend_ws.name)
158
+ else:
159
+ deps.web_search_tool = None
160
+ except Exception as exc:
161
+ logger.warning("web search backend init failed — disabling: %s", exc)
162
+ deps.web_search_tool = None
163
+
164
+ if vertex_gs:
165
+ logger.info("vertex google_search grounding enabled")
166
+ if deps.web_search_tool is None and not vertex_gs:
167
+ logger.info("web search disabled (WEB_SEARCH_BACKEND=none)")
168
+
169
+ if _memory_enabled():
170
+ try:
171
+ mem_uri = _memory_storage_uri()
172
+ storage = create_workspace_storage(mem_uri)
173
+ model_name = os.environ.get("MODEL_NAME", "gemini-2.5-flash")
174
+ memory = MemorySubsystem(
175
+ storage=storage,
176
+ provider=provider,
177
+ model=model_name,
178
+ memory_uri=mem_uri,
179
+ )
180
+ mgr = HookManager()
181
+ memory.register_hooks(mgr)
182
+ deps.hook_manager = mgr
183
+ deps.memory = memory
184
+ app.state.memory = memory
185
+ logger.info("memory hook enabled (memory_storage_uri=%s)", mem_uri)
186
+ except Exception as exc:
187
+ logger.warning("memory hook init failed — disabling: %s", exc)
188
+ else:
189
+ logger.info("memory hook disabled")
190
+
191
+ deps.subagent_registry = get_subagent_registry()
192
+
193
+ if attachments_enabled_from_env():
194
+ try:
195
+ deps.attachment_store = FilesystemAttachmentStore(resolve_agent_workspace_root())
196
+ app.state.attachment_store = deps.attachment_store
197
+ logger.info("attachments enabled")
198
+ except Exception as exc:
199
+ logger.warning("attachment store init failed: %s", exc)
200
+ deps.attachment_store = None
201
+ app.state.attachment_store = None
202
+ else:
203
+ deps.attachment_store = None
204
+ app.state.attachment_store = None
205
+ logger.info("attachments disabled via ATTACHMENTS_ENABLED")
206
+
207
+ deps.freeze()
208
+
209
+ try:
210
+ yield
211
+ finally:
212
+ if deps.mcp is not None:
213
+ for name in list(getattr(deps.mcp, "_servers", {}).keys()):
214
+ try:
215
+ await deps.mcp.disconnect(name)
216
+ except Exception:
217
+ logger.exception("MCP disconnect failed for %s", name)
218
+ if deps.storage is not None:
219
+ await deps.storage.close()
220
+
221
+
222
+ @contextlib.asynccontextmanager
223
+ async def _combined_lifespan(app: FastAPI) -> AsyncIterator[None]:
224
+ """SSE startup (turn-based /reply) plus realtime Live provider wiring.
225
+
226
+ Chat and talk share this process. SSE ``GatewayLoopPort`` reads module-level
227
+ ``_deps`` from ``gateway.sse.app``, so we must run SSE ``_startup`` here —
228
+ not only the realtime-only lifespan (which left /reply with a no-op loop).
229
+ """
230
+ from monkeybot.gateway.sse.app import _deps as sse_deps
231
+ from monkeybot.gateway.sse.app import _shutdown as sse_shutdown
232
+ from monkeybot.gateway.sse.app import _startup as sse_startup
233
+
234
+ deps: RealtimeDependencies = app.state.realtime_deps
235
+ config: RealtimeConfig = app.state.realtime_config
236
+
237
+ await sse_startup(app)
238
+
239
+ # Bridge shared process deps into RealtimeDependencies (no second DB open).
240
+ deps.storage = app.state.storage
241
+ deps.mcp = sse_deps.mcp
242
+ deps.inspectors = list(sse_deps.inspectors)
243
+ deps.memory = sse_deps.memory
244
+ deps.hook_manager = sse_deps.hook_manager
245
+ deps.web_search_tool = sse_deps.web_search_tool
246
+ deps.run_command_allowed_commands = sse_deps.run_command_allowed_commands
247
+ deps.run_command_allowed_path_prefixes = sse_deps.run_command_allowed_path_prefixes
248
+ deps.subagent_registry = sse_deps.subagent_registry
249
+ deps.attachment_store = getattr(app.state, "attachment_store", None)
250
+
251
+ realtime_provider = normalize_model_provider(
252
+ config.model.provider or os.environ.get("MODEL_PROVIDER", "google_vertexai")
253
+ )
254
+ if realtime_provider == "google_genai":
255
+ api_key = os.environ.get("GEMINI_API_KEY", "")
256
+ if not api_key:
257
+ logger.warning("realtime MODEL_PROVIDER=google_genai but GEMINI_API_KEY is not set")
258
+ deps.realtime_provider = GeminiLiveProvider(api_key=api_key)
259
+ else:
260
+ deps.realtime_provider = GeminiLiveProvider()
261
+
262
+ deps.freeze()
263
+
264
+ try:
265
+ yield
266
+ finally:
267
+ await sse_shutdown(app)
268
+
269
+
270
+ def create_realtime_app(**kwargs: Any) -> FastAPI:
271
+ """Build a FastAPI app serving both SSE routes and the realtime WebSocket endpoint.
272
+
273
+ Extra keyword arguments are forwarded to the underlying SSE app factory.
274
+ """
275
+ ensure_gateway_runtime_env()
276
+
277
+ config = get_realtime_config()
278
+ manager = RealtimeSessionManager(config)
279
+ deps = RealtimeDependencies()
280
+
281
+ # Wire the real turn-based loop (same as gateway.sse.app). Without this,
282
+ # build_sse_app installs a no-op default that clears busy and emits nothing —
283
+ # which leaves the chat TUI stuck on "thinking…".
284
+ from monkeybot.gateway.sse.app import GatewayLoopPort, _StaticUsagePortZeros
285
+ from monkeybot.gateway.sse.session_bus import SessionRegistry
286
+
287
+ registry = kwargs.pop("registry", None) or SessionRegistry()
288
+ loop_port = kwargs.pop("loop_port", None) or GatewayLoopPort(registry)
289
+ usage_port = kwargs.pop("usage_port", None) or _StaticUsagePortZeros()
290
+
291
+ app = build_sse_app(
292
+ lifespan=_combined_lifespan,
293
+ registry=registry,
294
+ loop_port=loop_port,
295
+ usage_port=usage_port,
296
+ **kwargs,
297
+ )
298
+ if isinstance(loop_port, GatewayLoopPort):
299
+ loop_port.bind_app(app)
300
+
301
+ app.state.realtime_config = config
302
+ app.state.realtime_deps = deps
303
+ app.state.realtime_manager = manager
304
+
305
+ if not any(isinstance(m, CORSMiddleware) for m in app.user_middleware):
306
+ app.add_middleware(
307
+ CORSMiddleware,
308
+ allow_origins=["*"],
309
+ allow_credentials=True,
310
+ allow_methods=["*"],
311
+ allow_headers=["*"],
312
+ )
313
+
314
+ router = create_realtime_router(deps, manager)
315
+ app.include_router(router)
316
+
317
+ return app
318
+
319
+
320
+ # Module-level app for ``uvicorn monkeybot.gateway.realtime.app:app``.
321
+ app = create_realtime_app()
@@ -0,0 +1,52 @@
1
+ """Process-level dependencies for the realtime gateway.
2
+
3
+ This mirrors the SSE gateway's ``_GatewayDeps`` but is owned by the realtime package so
4
+ that the realtime app can be wired independently without modifying ``gateway/sse/app.py``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+ from monkeybot.core.attachments.store import AttachmentStore
13
+ from monkeybot.core.config.settings import SubagentConfig
14
+ from monkeybot.core.hooks import HookManager
15
+ from monkeybot.core.llm.realtime_provider import RealtimeProvider
16
+ from monkeybot.core.mcp.ports_mcp import MCPClientPort
17
+ from monkeybot.core.memory.subsystem import MemorySubsystem
18
+ from monkeybot.core.persistence.backends import StorageBackend
19
+ from monkeybot.core.tools.inspector import ToolInspector
20
+
21
+
22
+ @dataclass
23
+ class RealtimeDependencies:
24
+ """Mutable process singleton populated on realtime app startup.
25
+
26
+ Call :meth:`freeze` after lifespan setup so accidental post-startup mutation fails
27
+ loudly (single-process assumption; multi-replica still needs sticky WS routing).
28
+ """
29
+
30
+ mcp: MCPClientPort | None = None
31
+ inspectors: list[ToolInspector] = field(default_factory=list)
32
+ memory: MemorySubsystem | None = None
33
+ hook_manager: HookManager | None = None
34
+ web_search_tool: Any | None = None
35
+ run_command_allowed_commands: list[str] | None = None
36
+ run_command_allowed_path_prefixes: list[str] | None = None
37
+ subagent_registry: dict[str, SubagentConfig] = field(default_factory=dict)
38
+ realtime_provider: RealtimeProvider | None = None
39
+ storage: StorageBackend | None = None
40
+ attachment_store: AttachmentStore | None = None
41
+ _frozen: bool = field(default=False, repr=False)
42
+
43
+ def freeze(self) -> None:
44
+ """Lock dependency fields after startup wiring completes."""
45
+ object.__setattr__(self, "_frozen", True)
46
+
47
+ def __setattr__(self, name: str, value: Any) -> None:
48
+ if name != "_frozen" and getattr(self, "_frozen", False):
49
+ raise RuntimeError(
50
+ f"RealtimeDependencies is frozen; cannot set {name!r} after startup"
51
+ )
52
+ object.__setattr__(self, name, value)