@aliwey/bmo 2.0.0
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.
- package/README.md +90 -0
- package/bin/bmo.js +188 -0
- package/cli.py +1129 -0
- package/config/__init__.py +0 -0
- package/config/__pycache__/__init__.cpython-313.pyc +0 -0
- package/config/__pycache__/settings.cpython-313.pyc +0 -0
- package/config/__pycache__/system-prompt.cpython-313.pyc +0 -0
- package/config/settings.py +104 -0
- package/config/system-prompt.json +18 -0
- package/core/__init__.py +0 -0
- package/core/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_a2a_bridge.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_agent_card.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_connector.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_discovery.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_identity.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_tasks.cpython-313.pyc +0 -0
- package/core/__pycache__/bfp_transport.cpython-313.pyc +0 -0
- package/core/__pycache__/bmo_engine.cpython-313.pyc +0 -0
- package/core/__pycache__/bot_client.cpython-313.pyc +0 -0
- package/core/__pycache__/budget_tracker.cpython-313.pyc +0 -0
- package/core/__pycache__/cli_renderer.cpython-313.pyc +0 -0
- package/core/__pycache__/goal_runner.cpython-313.pyc +0 -0
- package/core/__pycache__/request_worker.cpython-313.pyc +0 -0
- package/core/__pycache__/security.cpython-313.pyc +0 -0
- package/core/__pycache__/shared_state.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_manager.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_multiproc.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_protocol.cpython-313.pyc +0 -0
- package/core/__pycache__/worker_subprocess.cpython-313.pyc +0 -0
- package/core/bfp_a2a_bridge.py +399 -0
- package/core/bfp_agent.py +98 -0
- package/core/bfp_agent_card.py +161 -0
- package/core/bfp_connector.py +177 -0
- package/core/bfp_discovery.py +105 -0
- package/core/bfp_identity.py +83 -0
- package/core/bfp_tasks.py +70 -0
- package/core/bfp_transport.py +368 -0
- package/core/bmo_engine.py +405 -0
- package/core/bot_client.py +838 -0
- package/core/budget_tracker.py +62 -0
- package/core/cli_renderer.py +177 -0
- package/core/goal_runner.py +129 -0
- package/core/request_worker.py +242 -0
- package/core/security.py +42 -0
- package/core/shared_state.py +4 -0
- package/core/worker_manager.py +71 -0
- package/core/worker_multiproc.py +155 -0
- package/core/worker_protocol.py +30 -0
- package/core/worker_subprocess.py +222 -0
- package/handlers/__init__.py +0 -0
- package/handlers/__pycache__/__init__.cpython-313.pyc +0 -0
- package/handlers/__pycache__/messages.cpython-313.pyc +0 -0
- package/handlers/messages.py +2761 -0
- package/main.py +125 -0
- package/memory.md +43 -0
- package/models/__init__.py +0 -0
- package/models/__pycache__/__init__.cpython-313.pyc +0 -0
- package/models/__pycache__/chat_models.cpython-313.pyc +0 -0
- package/models/chat_models.py +143 -0
- package/package.json +50 -0
- package/registry/worker.js +108 -0
- package/registry/wrangler.toml +11 -0
- package/requirements.txt +13 -0
- package/scripts/bmo_init.js +115 -0
- package/scripts/postinstall.js +265 -0
- package/scripts/relay_cmd.js +276 -0
- package/scripts/web_cmd.js +136 -0
- package/setup.py +26 -0
- package/storage/__init__.py +0 -0
- package/storage/__pycache__/__init__.cpython-313.pyc +0 -0
- package/storage/__pycache__/sqlite_storage.cpython-313.pyc +0 -0
- package/storage/__pycache__/storage.cpython-313.pyc +0 -0
- package/storage/sqlite_storage.py +658 -0
- package/storage/storage.py +265 -0
- package/tools/__pycache__/bfp_relay.cpython-313.pyc +0 -0
- package/tools/__pycache__/get_session_summaries.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_bridge.cpython-313.pyc +0 -0
- package/tools/__pycache__/mcp_server.cpython-313.pyc +0 -0
- package/tools/__pycache__/run_mcp_standalone.cpython-313.pyc +0 -0
- package/tools/__pycache__/task_registry.cpython-313.pyc +0 -0
- package/tools/__pycache__/test_mcp_connection.cpython-313.pyc +0 -0
- package/tools/bfp_relay.py +359 -0
- package/tools/bot.db +0 -0
- package/tools/get_session_summaries.py +45 -0
- package/tools/mcp_bridge.py +109 -0
- package/tools/mcp_server.py +531 -0
- package/tools/register_mcp_task.py +20 -0
- package/tools/run_detached.bat +32 -0
- package/tools/run_mcp_standalone.py +16 -0
- package/tools/task_registry.py +184 -0
- package/tools/test_mcp_connection.py +80 -0
- package/webchat/package-lock.json +1528 -0
- package/webchat/package.json +12 -0
- package/webchat/public/app.js +1293 -0
- package/webchat/public/index.html +226 -0
- package/webchat/public/index.html.bak +416 -0
- package/webchat/public/styles.css +2435 -0
- package/webchat/server.js +645 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified business logic engine for all BMO interfaces (CLI, Telegram, Webchat).
|
|
3
|
+
Centralizes session management, model selection, SQLite DB queries, and OpenCode client calls.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import json
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Optional, List, Dict, AsyncGenerator
|
|
14
|
+
|
|
15
|
+
from config.settings import OPENCODE_BASE_URL, BFP_RELAY_URL, BFP_TRANSPORT_PORT, BFP_A2A_PORT
|
|
16
|
+
from core.bot_client import OpenCodeBotClient
|
|
17
|
+
from core.worker_manager import WorkerManager
|
|
18
|
+
from models.chat_models import ChatSession, ChatMessage
|
|
19
|
+
from storage.storage import get_storage
|
|
20
|
+
from core.bfp_agent import BFPAgent
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# Load shared system prompt config
|
|
25
|
+
_config_path = os.path.join(os.path.dirname(__file__), "..", "config", "system-prompt.json")
|
|
26
|
+
try:
|
|
27
|
+
with open(_config_path, "r", encoding="utf-8") as _f:
|
|
28
|
+
_system_config = json.load(_f)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
logger.error("Failed to load system prompt config: %s", e)
|
|
31
|
+
_system_config = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BMOEngine:
|
|
35
|
+
def __init__(self, db_path: Optional[str] = None, worker_backend: Optional[str] = None):
|
|
36
|
+
if db_path:
|
|
37
|
+
from storage.sqlite_storage import SQLiteStorage
|
|
38
|
+
self.storage = SQLiteStorage(db_path=db_path)
|
|
39
|
+
else:
|
|
40
|
+
self.storage = get_storage()
|
|
41
|
+
self.client = OpenCodeBotClient()
|
|
42
|
+
self.worker_manager = WorkerManager(backend=worker_backend)
|
|
43
|
+
self.client.set_worker_manager(self.worker_manager)
|
|
44
|
+
self._running_tasks: Dict[str, asyncio.Task] = {}
|
|
45
|
+
self._worker_started = False
|
|
46
|
+
self._bfp_started = False
|
|
47
|
+
self.bfp = BFPAgent()
|
|
48
|
+
|
|
49
|
+
async def ensure_worker(self):
|
|
50
|
+
# Don't start the worker if the client has it disabled (e.g. CLI uses
|
|
51
|
+
# `_send_direct` and sets `client._worker = None` to skip the worker).
|
|
52
|
+
# Starting it anyway would spawn a stranded subprocess whose pipes
|
|
53
|
+
# fire `__del__` warnings on interpreter shutdown.
|
|
54
|
+
if not self.client._worker:
|
|
55
|
+
return
|
|
56
|
+
# Start the worker if not started yet, OR if it was killed by a
|
|
57
|
+
# previous cancel (e.g. SIGINT in CLI). is_alive is the source of
|
|
58
|
+
# truth for whether the subprocess is actually running.
|
|
59
|
+
if not self._worker_started or not self.worker_manager.is_alive:
|
|
60
|
+
await self.worker_manager.start()
|
|
61
|
+
self._worker_started = True
|
|
62
|
+
if not self._bfp_started:
|
|
63
|
+
asyncio.create_task(self.bfp.start(
|
|
64
|
+
bfp_port=BFP_TRANSPORT_PORT,
|
|
65
|
+
a2a_port=BFP_A2A_PORT,
|
|
66
|
+
relay_url=BFP_RELAY_URL,
|
|
67
|
+
))
|
|
68
|
+
self._bfp_started = True
|
|
69
|
+
|
|
70
|
+
async def get_or_create_session(self, chat_id: int, user_id: int, username: Optional[str] = None) -> ChatSession:
|
|
71
|
+
"""Loads the active session for a chat. If none exists, creates a fresh one."""
|
|
72
|
+
session = self.storage.load_session(chat_id)
|
|
73
|
+
if session:
|
|
74
|
+
return session
|
|
75
|
+
return await self.create_new_session(chat_id, user_id, username)
|
|
76
|
+
|
|
77
|
+
async def create_new_session(self, chat_id: int, user_id: int, username: Optional[str] = None) -> ChatSession:
|
|
78
|
+
"""Creates a brand new session, auto-summarizing the previous one if applicable.
|
|
79
|
+
|
|
80
|
+
Soft-reset semantics (matches Claude Code / OpenCode CLI /new behavior):
|
|
81
|
+
- Local DB history is wiped (fresh chat row, empty messages).
|
|
82
|
+
- Backend model context is wiped via delete_messages (fresh turn 1).
|
|
83
|
+
- opencode_session_id is PRESERVED so the system prompt stays warm.
|
|
84
|
+
- Provider/model/agent/mode inherit from the previous session if available.
|
|
85
|
+
"""
|
|
86
|
+
# 1. Summarize old session if it contains messages and hasn't been summarized
|
|
87
|
+
old_session = self.storage.load_session(chat_id)
|
|
88
|
+
|
|
89
|
+
# 2. Capture preserved backend state from the old session (if any)
|
|
90
|
+
preserved_opencode_sid = None
|
|
91
|
+
inherited_provider = os.getenv("OPENCODE_PROVIDER", "opencode")
|
|
92
|
+
inherited_model = os.getenv("OPENCODE_MODEL", "big-pickle")
|
|
93
|
+
inherited_mode = "execute"
|
|
94
|
+
inherited_agent = "default"
|
|
95
|
+
|
|
96
|
+
if old_session:
|
|
97
|
+
preserved_opencode_sid = old_session.metadata.get("opencode_session_id")
|
|
98
|
+
inherited_provider = old_session.metadata.get("provider_id", inherited_provider)
|
|
99
|
+
inherited_model = old_session.metadata.get("model_id", inherited_model)
|
|
100
|
+
inherited_mode = old_session.metadata.get("active_mode", inherited_mode)
|
|
101
|
+
inherited_agent = old_session.metadata.get("active_agent", inherited_agent)
|
|
102
|
+
|
|
103
|
+
if old_session.messages and not old_session.get_summary():
|
|
104
|
+
await self.summarize_session(old_session.session_id)
|
|
105
|
+
|
|
106
|
+
# 3. Wipe backend model context for the preserved session (keeps the slot warm)
|
|
107
|
+
if preserved_opencode_sid:
|
|
108
|
+
try:
|
|
109
|
+
await self.client.delete_messages(preserved_opencode_sid)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.warning("Failed to wipe backend history on /new: %s", e)
|
|
112
|
+
# If the backend session is dead, fall back to a cold start
|
|
113
|
+
preserved_opencode_sid = None
|
|
114
|
+
self.client.last_session_id = None
|
|
115
|
+
|
|
116
|
+
# 4. Setup a new ChatSession row, inheriting backend + provider state
|
|
117
|
+
now = time.time()
|
|
118
|
+
new_sid = str(uuid4())
|
|
119
|
+
|
|
120
|
+
new_session = ChatSession(
|
|
121
|
+
chat_id=chat_id,
|
|
122
|
+
user_id=user_id,
|
|
123
|
+
username=username,
|
|
124
|
+
created_at=now,
|
|
125
|
+
updated_at=now,
|
|
126
|
+
messages=[],
|
|
127
|
+
metadata={
|
|
128
|
+
"opencode_session_id": preserved_opencode_sid,
|
|
129
|
+
"provider_id": inherited_provider,
|
|
130
|
+
"model_id": inherited_model,
|
|
131
|
+
"active_mode": inherited_mode,
|
|
132
|
+
"active_agent": inherited_agent
|
|
133
|
+
},
|
|
134
|
+
title=f"Session {datetime.now().strftime('%y%m%d_%H%M')}",
|
|
135
|
+
session_id=new_sid
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.storage.save_session(new_session)
|
|
139
|
+
self.storage.set_active_session(chat_id, new_sid)
|
|
140
|
+
return new_session
|
|
141
|
+
|
|
142
|
+
async def reset_opencode_session(self, chat_id: int):
|
|
143
|
+
"""Clears the cached OpenCode session ID so the next request gets a fresh server-side session.
|
|
144
|
+
Call this after a Ctrl+C cancellation to avoid queuing behind the abandoned request."""
|
|
145
|
+
session = self.storage.load_session(chat_id)
|
|
146
|
+
if session:
|
|
147
|
+
session.metadata["opencode_session_id"] = None
|
|
148
|
+
self.storage.save_session(session)
|
|
149
|
+
# Also clear client-side cache
|
|
150
|
+
self.client.last_session_id = None
|
|
151
|
+
|
|
152
|
+
async def switch_session(self, chat_id: int, session_id: str) -> Optional[ChatSession]:
|
|
153
|
+
"""Switches the active session pointer for a chat."""
|
|
154
|
+
if self.storage.switch_session(chat_id, session_id):
|
|
155
|
+
# Load active session from active_sessions table
|
|
156
|
+
session = self.storage.load_session(chat_id)
|
|
157
|
+
if session:
|
|
158
|
+
# Update global active sessions table
|
|
159
|
+
self._update_global_active_session(chat_id, session_id)
|
|
160
|
+
return session
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def _update_global_active_session(self, chat_id: int, session_id: str):
|
|
164
|
+
"""Internal helper to update active_sessions metadata for global synchronization."""
|
|
165
|
+
try:
|
|
166
|
+
conn = self.storage._conn()
|
|
167
|
+
# Ensure table has interface and updated_at (it should have been migrated in sqlite_storage.py)
|
|
168
|
+
conn.execute(
|
|
169
|
+
"INSERT OR REPLACE INTO active_sessions (chat_id, session_id) VALUES (?, ?)",
|
|
170
|
+
(chat_id, session_id)
|
|
171
|
+
)
|
|
172
|
+
conn.commit()
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error("Failed to sync active session metadata: %s", e)
|
|
175
|
+
|
|
176
|
+
def shutdown(self):
|
|
177
|
+
"""Shut down the worker manager and BFP agent (sync - fire-and-forget for atexit)."""
|
|
178
|
+
if hasattr(self, 'bfp') and self.bfp.is_running:
|
|
179
|
+
try:
|
|
180
|
+
loop = asyncio.get_event_loop()
|
|
181
|
+
if loop.is_running():
|
|
182
|
+
loop.create_task(self.bfp.stop())
|
|
183
|
+
else:
|
|
184
|
+
loop.run_until_complete(self.bfp.stop())
|
|
185
|
+
except RuntimeError:
|
|
186
|
+
loop = asyncio.new_event_loop()
|
|
187
|
+
loop.run_until_complete(self.bfp.stop())
|
|
188
|
+
loop.close()
|
|
189
|
+
if hasattr(self, 'worker_manager'):
|
|
190
|
+
try:
|
|
191
|
+
loop = asyncio.get_event_loop()
|
|
192
|
+
if loop.is_running():
|
|
193
|
+
loop.create_task(self.worker_manager.shutdown())
|
|
194
|
+
else:
|
|
195
|
+
loop.run_until_complete(self.worker_manager.shutdown())
|
|
196
|
+
except RuntimeError:
|
|
197
|
+
loop = asyncio.new_event_loop()
|
|
198
|
+
loop.run_until_complete(self.worker_manager.shutdown())
|
|
199
|
+
loop.close()
|
|
200
|
+
|
|
201
|
+
async def list_sessions(self, chat_id: int) -> List[dict]:
|
|
202
|
+
"""Lists all saved sessions for a chat."""
|
|
203
|
+
return self.storage.list_chat_sessions(chat_id)
|
|
204
|
+
|
|
205
|
+
async def set_session_title(self, session_id: str, title: str) -> bool:
|
|
206
|
+
"""Sets a custom title for a session."""
|
|
207
|
+
try:
|
|
208
|
+
self.storage.update_session_title(session_id, title)
|
|
209
|
+
return True
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error("Failed to set session title: %s", e)
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
async def clear_session_history(self, chat_id: int) -> bool:
|
|
215
|
+
"""Resets the history of the active session in DB and OpenCode server."""
|
|
216
|
+
session = self.storage.load_session(chat_id)
|
|
217
|
+
if not session:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# Clear backend messages if OpenCode session is active
|
|
221
|
+
opencode_sid = session.metadata.get("opencode_session_id")
|
|
222
|
+
if opencode_sid:
|
|
223
|
+
await self.client.delete_messages(opencode_sid)
|
|
224
|
+
|
|
225
|
+
# Remove local database messages for the session
|
|
226
|
+
try:
|
|
227
|
+
conn = self.storage._conn()
|
|
228
|
+
conn.execute("DELETE FROM messages WHERE session_id = ?", (session.session_id,))
|
|
229
|
+
conn.commit()
|
|
230
|
+
return True
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error("Failed to clear DB history: %s", e)
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
async def get_model_info(self, chat_id: int) -> tuple[str, str]:
|
|
236
|
+
"""Gets active model's provider and ID."""
|
|
237
|
+
session = self.storage.load_session(chat_id)
|
|
238
|
+
if not session:
|
|
239
|
+
env_p = os.getenv("OPENCODE_PROVIDER", "opencode")
|
|
240
|
+
env_m = os.getenv("OPENCODE_MODEL", "big-pickle")
|
|
241
|
+
return env_p, env_m
|
|
242
|
+
|
|
243
|
+
p_id = session.metadata.get("provider_id")
|
|
244
|
+
m_id = session.metadata.get("model_id")
|
|
245
|
+
if p_id and m_id:
|
|
246
|
+
return p_id, m_id
|
|
247
|
+
|
|
248
|
+
env_p = os.getenv("OPENCODE_PROVIDER")
|
|
249
|
+
env_m = os.getenv("OPENCODE_MODEL")
|
|
250
|
+
if env_p and env_m:
|
|
251
|
+
return env_p, env_m
|
|
252
|
+
|
|
253
|
+
return ("opencode", "big-pickle")
|
|
254
|
+
|
|
255
|
+
async def set_model(self, chat_id: int, provider_id: str, model_id: str) -> bool:
|
|
256
|
+
"""Switches the active model for a chat session."""
|
|
257
|
+
session = self.storage.load_session(chat_id)
|
|
258
|
+
if not session:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
session.metadata["provider_id"] = provider_id
|
|
262
|
+
session.metadata["model_id"] = model_id
|
|
263
|
+
session.metadata["opencode_session_id"] = None # Force fresh backend session creation
|
|
264
|
+
self.storage.save_session(session)
|
|
265
|
+
|
|
266
|
+
# Sync to environment for global consistency
|
|
267
|
+
os.environ["OPENCODE_PROVIDER"] = provider_id
|
|
268
|
+
os.environ["OPENCODE_MODEL"] = model_id
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
async def list_models(self) -> List[dict]:
|
|
272
|
+
"""Fetches available providers and models from OpenCode serve."""
|
|
273
|
+
providers_data = await self.client.get_providers()
|
|
274
|
+
return providers_data.get("all", [])
|
|
275
|
+
|
|
276
|
+
async def get_status(self) -> dict:
|
|
277
|
+
"""Returns OpenCode server connection status and stats."""
|
|
278
|
+
is_up = await self.client.is_alive()
|
|
279
|
+
stats = self.storage.get_stats()
|
|
280
|
+
return {
|
|
281
|
+
"connected": is_up,
|
|
282
|
+
"stats": stats,
|
|
283
|
+
"base_url": OPENCODE_BASE_URL
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async def get_agents(self) -> List[dict]:
|
|
287
|
+
"""Fetches available agents/skills from the server."""
|
|
288
|
+
return await self.client.get_agents()
|
|
289
|
+
|
|
290
|
+
async def summarize_session(self, session_id: str) -> str:
|
|
291
|
+
"""Generates an AI summary of a session and saves it."""
|
|
292
|
+
session = self.storage.get_session_by_id(session_id)
|
|
293
|
+
if not session or not session.messages:
|
|
294
|
+
return ""
|
|
295
|
+
|
|
296
|
+
history = session.get_context_text(max_messages=100)
|
|
297
|
+
prompt = (
|
|
298
|
+
"لخص هذه الجلسة البرمجية بشكل احترافي ومفصل باللغة العربية.\n"
|
|
299
|
+
"يجب أن يتضمن الملخص:\n"
|
|
300
|
+
"1. الهدف الرئيسي من الجلسة.\n"
|
|
301
|
+
"2. المشاكل التي تم حلها والكود الذي تم كتابته.\n"
|
|
302
|
+
"3. القرارات التقنية المهمة.\n"
|
|
303
|
+
"4. ما الذي يجب إكماله في الجلسة القادمة.\n\n"
|
|
304
|
+
f"السياق:\n{history}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
summary = await asyncio.wait_for(
|
|
309
|
+
self.client.send_query(prompt, active_mode="ask", chat_id=session.chat_id),
|
|
310
|
+
timeout=30.0
|
|
311
|
+
)
|
|
312
|
+
if summary and not summary.startswith("Error"):
|
|
313
|
+
self.storage.update_session_summary(session_id, summary)
|
|
314
|
+
return summary
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.warning("Summarization failed for session %s: %s", session_id, e)
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
async def send_message(
|
|
320
|
+
self,
|
|
321
|
+
chat_id: int,
|
|
322
|
+
user_id: int,
|
|
323
|
+
user_message: str,
|
|
324
|
+
interface: str = "cli",
|
|
325
|
+
files: Optional[list] = None
|
|
326
|
+
) -> str:
|
|
327
|
+
"""Sends a query to OpenCode and records conversation history in SQLite."""
|
|
328
|
+
return await self.send_message_streaming(
|
|
329
|
+
chat_id=chat_id,
|
|
330
|
+
user_id=user_id,
|
|
331
|
+
user_message=user_message,
|
|
332
|
+
interface=interface,
|
|
333
|
+
files=files,
|
|
334
|
+
on_token=None,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
async def send_message_streaming(
|
|
338
|
+
self,
|
|
339
|
+
chat_id: int,
|
|
340
|
+
user_id: int,
|
|
341
|
+
user_message: str,
|
|
342
|
+
interface: str = "cli",
|
|
343
|
+
files: Optional[list] = None,
|
|
344
|
+
on_token=None,
|
|
345
|
+
on_activity=None,
|
|
346
|
+
on_permission=None,
|
|
347
|
+
) -> str:
|
|
348
|
+
"""Sends a query with optional callbacks: on_token(partial_text), on_activity(kind, detail), on_permission(perm_id, perm_type, patterns) -> reply."""
|
|
349
|
+
await self.ensure_worker()
|
|
350
|
+
from typing import Callable
|
|
351
|
+
session = await self.get_or_create_session(chat_id, user_id)
|
|
352
|
+
|
|
353
|
+
# 1. Save user message to database
|
|
354
|
+
self.storage.add_message(chat_id, "user", user_message, interface=interface)
|
|
355
|
+
|
|
356
|
+
# 2. Build history context for continuity
|
|
357
|
+
history_messages = session.get_context_text(max_messages=5)
|
|
358
|
+
active_agent = session.metadata.get("active_agent", "default")
|
|
359
|
+
agent_info = None
|
|
360
|
+
if active_agent.startswith("custom_"):
|
|
361
|
+
try:
|
|
362
|
+
agent_id = int(active_agent.split("_")[1])
|
|
363
|
+
agent_info = self.storage.get_custom_agent_by_id(agent_id)
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
query_with_context = f"[Message Source: {interface.upper()}]\nPrevious context for continuity:\n{history_messages}\n\nLatest User Message: {user_message}"
|
|
368
|
+
if agent_info:
|
|
369
|
+
query_with_context = f"[CUSTOM AGENT SYSTEM PROMPT: {agent_info['system_prompt']}]\n\n{query_with_context}"
|
|
370
|
+
|
|
371
|
+
# Inject CLI interface context to lock BMO identity when using the terminal
|
|
372
|
+
if interface == "cli":
|
|
373
|
+
cli_ctx = _system_config.get("cli_context", "")
|
|
374
|
+
if cli_ctx:
|
|
375
|
+
query_with_context = cli_ctx + "\n\n" + query_with_context
|
|
376
|
+
|
|
377
|
+
opencode_sid = session.metadata.get("opencode_session_id")
|
|
378
|
+
provider_id, model_id = await self.get_model_info(chat_id)
|
|
379
|
+
active_mode = session.metadata.get("active_mode", "execute")
|
|
380
|
+
|
|
381
|
+
# 3. Call OpenCode server with optional streaming callback
|
|
382
|
+
response = await self.client.send_query(
|
|
383
|
+
query=query_with_context,
|
|
384
|
+
session_id=opencode_sid,
|
|
385
|
+
provider_id=provider_id,
|
|
386
|
+
model_id=model_id,
|
|
387
|
+
active_mode=active_mode,
|
|
388
|
+
active_agent=active_agent,
|
|
389
|
+
chat_id=chat_id,
|
|
390
|
+
session_uuid=session.session_id,
|
|
391
|
+
files=files,
|
|
392
|
+
on_token=on_token,
|
|
393
|
+
on_activity=on_activity,
|
|
394
|
+
on_permission=on_permission,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# 4. Save backend session ID if it was created/changed
|
|
398
|
+
if self.client.last_session_id:
|
|
399
|
+
session.metadata["opencode_session_id"] = self.client.last_session_id
|
|
400
|
+
self.storage.save_session(session)
|
|
401
|
+
|
|
402
|
+
# 5. Save assistant response to database
|
|
403
|
+
self.storage.add_message(chat_id, "assistant", response, interface=interface)
|
|
404
|
+
|
|
405
|
+
return response
|