vibe-remote 2.1.6__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.
- config/__init__.py +37 -0
- config/paths.py +56 -0
- config/v2_compat.py +74 -0
- config/v2_config.py +206 -0
- config/v2_sessions.py +73 -0
- config/v2_settings.py +115 -0
- core/__init__.py +0 -0
- core/controller.py +736 -0
- core/handlers/__init__.py +13 -0
- core/handlers/command_handlers.py +342 -0
- core/handlers/message_handler.py +365 -0
- core/handlers/session_handler.py +233 -0
- core/handlers/settings_handler.py +362 -0
- modules/__init__.py +0 -0
- modules/agent_router.py +58 -0
- modules/agents/__init__.py +38 -0
- modules/agents/base.py +91 -0
- modules/agents/claude_agent.py +344 -0
- modules/agents/codex_agent.py +368 -0
- modules/agents/opencode_agent.py +2155 -0
- modules/agents/service.py +41 -0
- modules/agents/subagent_router.py +136 -0
- modules/claude_client.py +154 -0
- modules/im/__init__.py +63 -0
- modules/im/base.py +323 -0
- modules/im/factory.py +60 -0
- modules/im/formatters/__init__.py +4 -0
- modules/im/formatters/base_formatter.py +639 -0
- modules/im/formatters/slack_formatter.py +127 -0
- modules/im/slack.py +2091 -0
- modules/session_manager.py +138 -0
- modules/settings_manager.py +587 -0
- vibe/__init__.py +6 -0
- vibe/__main__.py +12 -0
- vibe/_version.py +34 -0
- vibe/api.py +412 -0
- vibe/cli.py +637 -0
- vibe/runtime.py +213 -0
- vibe/service_main.py +101 -0
- vibe/templates/slack_manifest.json +65 -0
- vibe/ui/dist/assets/index-8g3mNwMK.js +35 -0
- vibe/ui/dist/assets/index-M55aMB5R.css +1 -0
- vibe/ui/dist/assets/logo-BzryTZ7u.png +0 -0
- vibe/ui/dist/index.html +17 -0
- vibe/ui/dist/logo.png +0 -0
- vibe/ui/dist/vite.svg +1 -0
- vibe/ui_server.py +346 -0
- vibe_remote-2.1.6.dist-info/METADATA +295 -0
- vibe_remote-2.1.6.dist-info/RECORD +52 -0
- vibe_remote-2.1.6.dist-info/WHEEL +4 -0
- vibe_remote-2.1.6.dist-info/entry_points.txt +2 -0
- vibe_remote-2.1.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Settings and configuration handlers"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from modules.agents import get_agent_display_name
|
|
5
|
+
from modules.im import MessageContext, InlineKeyboard, InlineButton
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SettingsHandler:
|
|
11
|
+
"""Handles settings and configuration operations"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, controller):
|
|
14
|
+
"""Initialize with reference to main controller"""
|
|
15
|
+
self.controller = controller
|
|
16
|
+
self.config = controller.config
|
|
17
|
+
self.im_client = controller.im_client
|
|
18
|
+
self.settings_manager = controller.settings_manager
|
|
19
|
+
self.formatter = controller.im_client.formatter
|
|
20
|
+
|
|
21
|
+
def _get_settings_key(self, context: MessageContext) -> str:
|
|
22
|
+
"""Get settings key - delegate to controller"""
|
|
23
|
+
return self.controller._get_settings_key(context)
|
|
24
|
+
|
|
25
|
+
def _get_agent_display_name(self, context: MessageContext) -> str:
|
|
26
|
+
"""Return a friendly agent name for the current context."""
|
|
27
|
+
agent_name = self.controller.resolve_agent_for_context(context)
|
|
28
|
+
default_agent = getattr(self.controller.agent_service, "default_agent", None)
|
|
29
|
+
return get_agent_display_name(agent_name, fallback=default_agent)
|
|
30
|
+
|
|
31
|
+
async def handle_settings(self, context: MessageContext, args: str = ""):
|
|
32
|
+
"""Handle settings command - show settings menu"""
|
|
33
|
+
try:
|
|
34
|
+
# For Slack, use modal dialog
|
|
35
|
+
if self.config.platform == "slack":
|
|
36
|
+
await self._handle_settings_slack(context)
|
|
37
|
+
else:
|
|
38
|
+
# For other platforms, use inline keyboard
|
|
39
|
+
await self._handle_settings_traditional(context)
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Error showing settings: {e}")
|
|
43
|
+
await self.im_client.send_message(
|
|
44
|
+
context, f"❌ Error showing settings: {str(e)}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def _handle_settings_traditional(self, context: MessageContext):
|
|
48
|
+
"""Handle settings for non-Slack platforms"""
|
|
49
|
+
# Get current settings
|
|
50
|
+
settings_key = self._get_settings_key(context)
|
|
51
|
+
user_settings = self.settings_manager.get_user_settings(settings_key)
|
|
52
|
+
|
|
53
|
+
# Get available message types and display names
|
|
54
|
+
message_types = self.settings_manager.get_available_message_types()
|
|
55
|
+
display_names = self.settings_manager.get_message_type_display_names()
|
|
56
|
+
|
|
57
|
+
# Create inline keyboard buttons in 2x2 layout
|
|
58
|
+
buttons = []
|
|
59
|
+
row = []
|
|
60
|
+
|
|
61
|
+
for i, msg_type in enumerate(message_types):
|
|
62
|
+
is_shown = msg_type in user_settings.show_message_types
|
|
63
|
+
checkbox = "☑️" if is_shown else "⬜"
|
|
64
|
+
display_name = display_names.get(msg_type, msg_type)
|
|
65
|
+
button = InlineButton(
|
|
66
|
+
text=f"{checkbox} Show {display_name}",
|
|
67
|
+
callback_data=f"toggle_msg_{msg_type}",
|
|
68
|
+
)
|
|
69
|
+
row.append(button)
|
|
70
|
+
|
|
71
|
+
# Create 2x2 layout
|
|
72
|
+
if len(row) == 2 or i == len(message_types) - 1:
|
|
73
|
+
buttons.append(row)
|
|
74
|
+
row = []
|
|
75
|
+
|
|
76
|
+
# Add info button on its own row
|
|
77
|
+
buttons.append(
|
|
78
|
+
[InlineButton("ℹ️ About Message Types", callback_data="info_msg_types")]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
keyboard = InlineKeyboard(buttons=buttons)
|
|
82
|
+
|
|
83
|
+
# Send settings message with escaped dash
|
|
84
|
+
agent_label = self._get_agent_display_name(context)
|
|
85
|
+
await self.im_client.send_message_with_buttons(
|
|
86
|
+
context,
|
|
87
|
+
f"⚙️ *Settings \\- Message Visibility*\n\nSelect which message types to hide from {agent_label} output:",
|
|
88
|
+
keyboard,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def _handle_settings_slack(self, context: MessageContext):
|
|
92
|
+
"""Handle settings for Slack using modal dialog"""
|
|
93
|
+
# For slash commands or direct triggers, we might have trigger_id
|
|
94
|
+
trigger_id = (
|
|
95
|
+
context.platform_specific.get("trigger_id")
|
|
96
|
+
if context.platform_specific
|
|
97
|
+
else None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if trigger_id and hasattr(self.im_client, "open_settings_modal"):
|
|
101
|
+
# We have trigger_id, open modal directly
|
|
102
|
+
settings_key = self._get_settings_key(context)
|
|
103
|
+
user_settings = self.settings_manager.get_user_settings(settings_key)
|
|
104
|
+
message_types = self.settings_manager.get_available_message_types()
|
|
105
|
+
display_names = self.settings_manager.get_message_type_display_names()
|
|
106
|
+
|
|
107
|
+
# Get current require_mention override for this channel
|
|
108
|
+
current_require_mention = self.settings_manager.get_require_mention_override(settings_key)
|
|
109
|
+
global_require_mention = self.config.slack.require_mention
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
await self.im_client.open_settings_modal(
|
|
113
|
+
trigger_id,
|
|
114
|
+
user_settings,
|
|
115
|
+
message_types,
|
|
116
|
+
display_names,
|
|
117
|
+
context.channel_id,
|
|
118
|
+
current_require_mention=current_require_mention,
|
|
119
|
+
global_require_mention=global_require_mention,
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Error opening settings modal: {e}")
|
|
123
|
+
await self.im_client.send_message(
|
|
124
|
+
context, "❌ Failed to open settings. Please try again."
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
# No trigger_id, show button to open modal
|
|
128
|
+
buttons = [
|
|
129
|
+
[
|
|
130
|
+
InlineButton(
|
|
131
|
+
text="🛠️ Open Settings", callback_data="open_settings_modal"
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
keyboard = InlineKeyboard(buttons=buttons)
|
|
137
|
+
|
|
138
|
+
await self.im_client.send_message_with_buttons(
|
|
139
|
+
context,
|
|
140
|
+
f"⚙️ *Personalization Settings*\n\nConfigure how {self._get_agent_display_name(context)} messages appear in your Slack workspace.",
|
|
141
|
+
keyboard,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def handle_toggle_message_type(self, context: MessageContext, msg_type: str):
|
|
145
|
+
"""Handle toggle for message type visibility"""
|
|
146
|
+
try:
|
|
147
|
+
# Toggle message type visibility
|
|
148
|
+
settings_key = self._get_settings_key(context)
|
|
149
|
+
is_shown = self.settings_manager.toggle_show_message_type(
|
|
150
|
+
settings_key, msg_type
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Update the keyboard
|
|
154
|
+
user_settings = self.settings_manager.get_user_settings(settings_key)
|
|
155
|
+
message_types = self.settings_manager.get_available_message_types()
|
|
156
|
+
display_names = self.settings_manager.get_message_type_display_names()
|
|
157
|
+
|
|
158
|
+
buttons = []
|
|
159
|
+
row = []
|
|
160
|
+
|
|
161
|
+
for i, mt in enumerate(message_types):
|
|
162
|
+
is_shown_now = mt in user_settings.show_message_types
|
|
163
|
+
checkbox = "☑️" if is_shown_now else "⬜"
|
|
164
|
+
display_name = display_names.get(mt, mt)
|
|
165
|
+
button = InlineButton(
|
|
166
|
+
text=f"{checkbox} Show {display_name}",
|
|
167
|
+
callback_data=f"toggle_msg_{mt}",
|
|
168
|
+
)
|
|
169
|
+
row.append(button)
|
|
170
|
+
|
|
171
|
+
# Create 2x2 layout
|
|
172
|
+
if len(row) == 2 or i == len(message_types) - 1:
|
|
173
|
+
buttons.append(row)
|
|
174
|
+
row = []
|
|
175
|
+
|
|
176
|
+
buttons.append(
|
|
177
|
+
[InlineButton("ℹ️ About Message Types", callback_data="info_msg_types")]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
keyboard = InlineKeyboard(buttons=buttons)
|
|
181
|
+
|
|
182
|
+
# Update message
|
|
183
|
+
if context.message_id:
|
|
184
|
+
await self.im_client.edit_message(
|
|
185
|
+
context, context.message_id, keyboard=keyboard
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Answer callback (for Telegram)
|
|
189
|
+
display_name = display_names.get(msg_type, msg_type)
|
|
190
|
+
action = "shown" if is_shown else "hidden"
|
|
191
|
+
|
|
192
|
+
# Platform-specific callback answering
|
|
193
|
+
await self.im_client.send_message(
|
|
194
|
+
context, f"{display_name} messages are now {action}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Error toggling message type {msg_type}: {e}")
|
|
199
|
+
await self.im_client.send_message(
|
|
200
|
+
context,
|
|
201
|
+
self.formatter.format_error(f"Failed to toggle setting: {str(e)}"),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def handle_info_message_types(self, context: MessageContext):
|
|
205
|
+
"""Show information about different message types"""
|
|
206
|
+
try:
|
|
207
|
+
formatter = self.im_client.formatter
|
|
208
|
+
|
|
209
|
+
# Use the new format_info_message method for clean, platform-agnostic formatting
|
|
210
|
+
info_text = formatter.format_info_message(
|
|
211
|
+
title="Message Types Info:",
|
|
212
|
+
emoji="📋",
|
|
213
|
+
items=[
|
|
214
|
+
("System", "System initialization and status messages"),
|
|
215
|
+
("Toolcall", "Agent tool name + params (one line)"),
|
|
216
|
+
("Assistant", "Agent responses and explanations"),
|
|
217
|
+
("Result", "Final execution result (always sent)"),
|
|
218
|
+
],
|
|
219
|
+
footer="Hidden messages won't be sent to your IM platform.",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Send as new message
|
|
223
|
+
await self.im_client.send_message(context, info_text)
|
|
224
|
+
logger.info(f"Sent info_msg_types message to user {context.user_id}")
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error in info_msg_types handler: {e}", exc_info=True)
|
|
228
|
+
await self.im_client.send_message(
|
|
229
|
+
context, "❌ Error showing message types info"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def handle_info_how_it_works(self, context: MessageContext):
|
|
233
|
+
"""Show information about how the bot works"""
|
|
234
|
+
try:
|
|
235
|
+
formatter = self.im_client.formatter
|
|
236
|
+
agent_label = self._get_agent_display_name(context)
|
|
237
|
+
|
|
238
|
+
# Use format_info_message for clean, platform-agnostic formatting
|
|
239
|
+
info_text = formatter.format_info_message(
|
|
240
|
+
title="How Vibe Remote Works:",
|
|
241
|
+
emoji="📚",
|
|
242
|
+
items=[
|
|
243
|
+
("Real-time", f"Messages are immediately sent to {agent_label}"),
|
|
244
|
+
("Persistent", "Each chat maintains its own conversation context"),
|
|
245
|
+
("Commands", "Use @Vibe Remote /start for menu, @Vibe Remote /clear to reset session"),
|
|
246
|
+
("Work Dir", "Change working directory with /set_cwd or via menu"),
|
|
247
|
+
("Settings", "Customize message visibility in Settings"),
|
|
248
|
+
],
|
|
249
|
+
footer=f"Just type normally to chat with {agent_label}!",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Send as new message
|
|
253
|
+
await self.im_client.send_message(context, info_text)
|
|
254
|
+
logger.info(f"Sent how_it_works info to user {context.user_id}")
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error(f"Error in handle_info_how_it_works: {e}", exc_info=True)
|
|
258
|
+
await self.im_client.send_message(
|
|
259
|
+
context, "❌ Error showing help information"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
async def handle_routing(self, context: MessageContext):
|
|
263
|
+
"""Handle routing command - show agent/model selection"""
|
|
264
|
+
try:
|
|
265
|
+
# Only Slack has modal support for now
|
|
266
|
+
if self.config.platform == "slack":
|
|
267
|
+
await self._handle_routing_slack(context)
|
|
268
|
+
else:
|
|
269
|
+
# For other platforms, show a simple message
|
|
270
|
+
await self.im_client.send_message(
|
|
271
|
+
context,
|
|
272
|
+
"🤖 Agent switching is currently only available in Slack. "
|
|
273
|
+
"Use Slack Agent Settings to configure routing.",
|
|
274
|
+
)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Error showing routing settings: {e}", exc_info=True)
|
|
277
|
+
await self.im_client.send_message(
|
|
278
|
+
context, f"❌ Error showing routing settings: {str(e)}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
async def _handle_routing_slack(self, context: MessageContext):
|
|
282
|
+
"""Handle routing for Slack using modal dialog"""
|
|
283
|
+
trigger_id = (
|
|
284
|
+
context.platform_specific.get("trigger_id")
|
|
285
|
+
if context.platform_specific
|
|
286
|
+
else None
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if not trigger_id:
|
|
290
|
+
# No trigger_id, show button to open modal
|
|
291
|
+
buttons = [
|
|
292
|
+
[
|
|
293
|
+
InlineButton(
|
|
294
|
+
text="🤖 Open Agent Settings",
|
|
295
|
+
callback_data="open_routing_modal",
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
]
|
|
299
|
+
keyboard = InlineKeyboard(buttons=buttons)
|
|
300
|
+
await self.im_client.send_message_with_buttons(
|
|
301
|
+
context,
|
|
302
|
+
"🤖 *Agent & Model Settings*\n\nConfigure which backend to use for this channel.",
|
|
303
|
+
keyboard,
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
# Gather data for the modal
|
|
308
|
+
settings_key = self._get_settings_key(context)
|
|
309
|
+
current_routing = self.settings_manager.get_channel_routing(settings_key)
|
|
310
|
+
|
|
311
|
+
# Get registered backends, prioritize opencode first
|
|
312
|
+
all_backends = list(self.controller.agent_service.agents.keys())
|
|
313
|
+
registered_backends = sorted(
|
|
314
|
+
all_backends, key=lambda x: (x != "opencode", x)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Get current backend (from routing or default)
|
|
318
|
+
current_backend = self.controller.resolve_agent_for_context(context)
|
|
319
|
+
|
|
320
|
+
# Get current require_mention override for this channel
|
|
321
|
+
current_require_mention = self.settings_manager.get_require_mention_override(settings_key)
|
|
322
|
+
global_require_mention = self.config.slack.require_mention
|
|
323
|
+
|
|
324
|
+
# Get OpenCode agents/models if available
|
|
325
|
+
opencode_agents = []
|
|
326
|
+
opencode_models = {}
|
|
327
|
+
opencode_default_config = {}
|
|
328
|
+
|
|
329
|
+
if "opencode" in registered_backends:
|
|
330
|
+
try:
|
|
331
|
+
# Get OpenCode server manager
|
|
332
|
+
opencode_agent = self.controller.agent_service.agents.get("opencode")
|
|
333
|
+
if opencode_agent and hasattr(opencode_agent, "_get_server"):
|
|
334
|
+
server = await opencode_agent._get_server()
|
|
335
|
+
await server.ensure_running()
|
|
336
|
+
|
|
337
|
+
cwd = self.controller.get_cwd(context)
|
|
338
|
+
opencode_agents = await server.get_available_agents(cwd)
|
|
339
|
+
opencode_models = await server.get_available_models(cwd)
|
|
340
|
+
opencode_default_config = await server.get_default_config(cwd)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.warning(f"Failed to fetch OpenCode data: {e}")
|
|
343
|
+
|
|
344
|
+
# Open modal
|
|
345
|
+
try:
|
|
346
|
+
await self.im_client.open_routing_modal(
|
|
347
|
+
trigger_id=trigger_id,
|
|
348
|
+
channel_id=context.channel_id,
|
|
349
|
+
registered_backends=registered_backends,
|
|
350
|
+
current_backend=current_backend,
|
|
351
|
+
current_routing=current_routing,
|
|
352
|
+
opencode_agents=opencode_agents,
|
|
353
|
+
opencode_models=opencode_models,
|
|
354
|
+
opencode_default_config=opencode_default_config,
|
|
355
|
+
current_require_mention=current_require_mention,
|
|
356
|
+
global_require_mention=global_require_mention,
|
|
357
|
+
)
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Error opening routing modal: {e}", exc_info=True)
|
|
360
|
+
await self.im_client.send_message(
|
|
361
|
+
context, "❌ Failed to open settings. Please try again."
|
|
362
|
+
)
|
modules/__init__.py
ADDED
|
File without changes
|
modules/agent_router.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Dict, Optional
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PlatformRoute:
|
|
12
|
+
default: str = "claude"
|
|
13
|
+
overrides: Dict[str, str] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentRouter:
|
|
17
|
+
"""Resolve which agent should serve a given message context."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
platform_routes: Dict[str, PlatformRoute],
|
|
22
|
+
global_default: str = "claude",
|
|
23
|
+
):
|
|
24
|
+
self.platform_routes = platform_routes
|
|
25
|
+
self.global_default = global_default
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_file(
|
|
29
|
+
cls, file_path: Optional[str], *, platform: str
|
|
30
|
+
) -> "AgentRouter":
|
|
31
|
+
routes: Dict[str, PlatformRoute] = {}
|
|
32
|
+
global_default = "claude"
|
|
33
|
+
|
|
34
|
+
# File-based routing removed; keep defaults only.
|
|
35
|
+
routes.setdefault(platform, PlatformRoute(default=global_default))
|
|
36
|
+
return cls(routes, global_default=global_default)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _load_file(path: str) -> Dict:
|
|
40
|
+
_, ext = os.path.splitext(path)
|
|
41
|
+
if ext.lower() in {".yaml", ".yml"}:
|
|
42
|
+
try:
|
|
43
|
+
import yaml # type: ignore
|
|
44
|
+
except ImportError as exc:
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
"PyYAML is required to parse YAML agent route files. "
|
|
47
|
+
"Install with `pip install pyyaml` or use JSON."
|
|
48
|
+
) from exc
|
|
49
|
+
with open(path, "r") as f:
|
|
50
|
+
return yaml.safe_load(f) or {}
|
|
51
|
+
with open(path, "r") as f:
|
|
52
|
+
return json.load(f)
|
|
53
|
+
|
|
54
|
+
def resolve(self, platform: str, channel_id: str) -> str:
|
|
55
|
+
platform_route = self.platform_routes.get(platform)
|
|
56
|
+
if not platform_route:
|
|
57
|
+
return self.global_default
|
|
58
|
+
return platform_route.overrides.get(channel_id, platform_route.default)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .base import BaseAgent, AgentRequest, AgentMessage
|
|
4
|
+
from .claude_agent import ClaudeAgent
|
|
5
|
+
from .codex_agent import CodexAgent
|
|
6
|
+
from .opencode_agent import OpenCodeAgent
|
|
7
|
+
from .service import AgentService
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_agent_display_name(agent_name: Optional[str], fallback: Optional[str] = None) -> str:
|
|
11
|
+
normalized_map = {
|
|
12
|
+
"claude": "Claude",
|
|
13
|
+
"codex": "Codex",
|
|
14
|
+
"opencode": "OpenCode",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
candidate = (agent_name or fallback or "Agent").strip()
|
|
18
|
+
if not candidate:
|
|
19
|
+
candidate = "Agent"
|
|
20
|
+
|
|
21
|
+
normalized = candidate.lower()
|
|
22
|
+
friendly = normalized_map.get(normalized)
|
|
23
|
+
if friendly:
|
|
24
|
+
return friendly
|
|
25
|
+
|
|
26
|
+
return candidate.replace("_", " ").title()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AgentMessage",
|
|
31
|
+
"AgentRequest",
|
|
32
|
+
"BaseAgent",
|
|
33
|
+
"ClaudeAgent",
|
|
34
|
+
"CodexAgent",
|
|
35
|
+
"OpenCodeAgent",
|
|
36
|
+
"AgentService",
|
|
37
|
+
"get_agent_display_name",
|
|
38
|
+
]
|
modules/agents/base.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Abstract agent interfaces and shared dataclasses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from modules.im import MessageContext
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AgentRequest:
|
|
15
|
+
"""Normalized agent invocation request."""
|
|
16
|
+
|
|
17
|
+
context: MessageContext
|
|
18
|
+
message: str
|
|
19
|
+
working_path: str
|
|
20
|
+
base_session_id: str
|
|
21
|
+
composite_session_id: str
|
|
22
|
+
settings_key: str
|
|
23
|
+
ack_message_id: Optional[str] = None
|
|
24
|
+
subagent_name: Optional[str] = None
|
|
25
|
+
subagent_key: Optional[str] = None
|
|
26
|
+
subagent_model: Optional[str] = None
|
|
27
|
+
subagent_reasoning_effort: Optional[str] = None
|
|
28
|
+
last_agent_message: Optional[str] = None
|
|
29
|
+
last_agent_message_parse_mode: Optional[str] = None
|
|
30
|
+
started_at: float = field(default_factory=time.monotonic)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class AgentMessage:
|
|
35
|
+
"""Normalized message emitted by an agent implementation."""
|
|
36
|
+
|
|
37
|
+
text: str
|
|
38
|
+
message_type: str = "assistant"
|
|
39
|
+
parse_mode: str = "markdown"
|
|
40
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BaseAgent(ABC):
|
|
44
|
+
"""Abstract base class for all agent implementations."""
|
|
45
|
+
|
|
46
|
+
name: str
|
|
47
|
+
|
|
48
|
+
def __init__(self, controller):
|
|
49
|
+
self.controller = controller
|
|
50
|
+
self.config = controller.config
|
|
51
|
+
self.im_client = controller.im_client
|
|
52
|
+
self.settings_manager = controller.settings_manager
|
|
53
|
+
|
|
54
|
+
def _calculate_duration_ms(self, started_at: Optional[float]) -> int:
|
|
55
|
+
if not started_at:
|
|
56
|
+
return 0
|
|
57
|
+
elapsed = time.monotonic() - started_at
|
|
58
|
+
return max(0, int(elapsed * 1000))
|
|
59
|
+
|
|
60
|
+
async def emit_result_message(
|
|
61
|
+
self,
|
|
62
|
+
context: MessageContext,
|
|
63
|
+
result_text: Optional[str],
|
|
64
|
+
subtype: str = "success",
|
|
65
|
+
duration_ms: Optional[int] = None,
|
|
66
|
+
started_at: Optional[float] = None,
|
|
67
|
+
parse_mode: str = "markdown",
|
|
68
|
+
suffix: Optional[str] = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
if duration_ms is None:
|
|
71
|
+
duration_ms = self._calculate_duration_ms(started_at)
|
|
72
|
+
formatted = self.im_client.formatter.format_result_message(
|
|
73
|
+
subtype or "", duration_ms, result_text
|
|
74
|
+
)
|
|
75
|
+
if suffix:
|
|
76
|
+
formatted = f"{formatted}\n{suffix}"
|
|
77
|
+
await self.controller.emit_agent_message(
|
|
78
|
+
context, "result", formatted, parse_mode=parse_mode
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
async def handle_message(self, request: AgentRequest) -> None:
|
|
83
|
+
"""Process a user message routed to this agent."""
|
|
84
|
+
|
|
85
|
+
async def clear_sessions(self, settings_key: str) -> int:
|
|
86
|
+
"""Clear session state for a given settings key. Returns cleared count."""
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
async def handle_stop(self, request: AgentRequest) -> bool:
|
|
90
|
+
"""Attempt to interrupt an in-flight task. Returns True if handled."""
|
|
91
|
+
return False
|