mindroom 0.0.0__py3-none-any.whl → 0.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.
- mindroom/__init__.py +3 -0
- mindroom/agent_prompts.py +963 -0
- mindroom/agents.py +248 -0
- mindroom/ai.py +421 -0
- mindroom/api/__init__.py +1 -0
- mindroom/api/credentials.py +137 -0
- mindroom/api/google_integration.py +355 -0
- mindroom/api/google_tools_helper.py +40 -0
- mindroom/api/homeassistant_integration.py +421 -0
- mindroom/api/integrations.py +189 -0
- mindroom/api/main.py +506 -0
- mindroom/api/matrix_operations.py +219 -0
- mindroom/api/tools.py +94 -0
- mindroom/background_tasks.py +87 -0
- mindroom/bot.py +2470 -0
- mindroom/cli.py +86 -0
- mindroom/commands.py +377 -0
- mindroom/config.py +343 -0
- mindroom/config_commands.py +324 -0
- mindroom/config_confirmation.py +411 -0
- mindroom/constants.py +52 -0
- mindroom/credentials.py +146 -0
- mindroom/credentials_sync.py +134 -0
- mindroom/custom_tools/__init__.py +8 -0
- mindroom/custom_tools/config_manager.py +765 -0
- mindroom/custom_tools/gmail.py +92 -0
- mindroom/custom_tools/google_calendar.py +92 -0
- mindroom/custom_tools/google_sheets.py +92 -0
- mindroom/custom_tools/homeassistant.py +341 -0
- mindroom/error_handling.py +35 -0
- mindroom/file_watcher.py +49 -0
- mindroom/interactive.py +313 -0
- mindroom/logging_config.py +207 -0
- mindroom/matrix/__init__.py +1 -0
- mindroom/matrix/client.py +782 -0
- mindroom/matrix/event_info.py +173 -0
- mindroom/matrix/identity.py +149 -0
- mindroom/matrix/large_messages.py +267 -0
- mindroom/matrix/mentions.py +141 -0
- mindroom/matrix/message_builder.py +94 -0
- mindroom/matrix/message_content.py +209 -0
- mindroom/matrix/presence.py +178 -0
- mindroom/matrix/rooms.py +311 -0
- mindroom/matrix/state.py +77 -0
- mindroom/matrix/typing.py +91 -0
- mindroom/matrix/users.py +217 -0
- mindroom/memory/__init__.py +21 -0
- mindroom/memory/config.py +137 -0
- mindroom/memory/functions.py +396 -0
- mindroom/py.typed +0 -0
- mindroom/response_tracker.py +128 -0
- mindroom/room_cleanup.py +139 -0
- mindroom/routing.py +107 -0
- mindroom/scheduling.py +758 -0
- mindroom/stop.py +207 -0
- mindroom/streaming.py +203 -0
- mindroom/teams.py +749 -0
- mindroom/thread_utils.py +318 -0
- mindroom/tools/__init__.py +520 -0
- mindroom/tools/agentql.py +64 -0
- mindroom/tools/airflow.py +57 -0
- mindroom/tools/apify.py +49 -0
- mindroom/tools/arxiv.py +64 -0
- mindroom/tools/aws_lambda.py +41 -0
- mindroom/tools/aws_ses.py +57 -0
- mindroom/tools/baidusearch.py +87 -0
- mindroom/tools/brightdata.py +116 -0
- mindroom/tools/browserbase.py +62 -0
- mindroom/tools/cal_com.py +98 -0
- mindroom/tools/calculator.py +112 -0
- mindroom/tools/cartesia.py +84 -0
- mindroom/tools/composio.py +166 -0
- mindroom/tools/config_manager.py +44 -0
- mindroom/tools/confluence.py +73 -0
- mindroom/tools/crawl4ai.py +101 -0
- mindroom/tools/csv.py +104 -0
- mindroom/tools/custom_api.py +106 -0
- mindroom/tools/dalle.py +85 -0
- mindroom/tools/daytona.py +180 -0
- mindroom/tools/discord.py +81 -0
- mindroom/tools/docker.py +73 -0
- mindroom/tools/duckdb.py +124 -0
- mindroom/tools/duckduckgo.py +99 -0
- mindroom/tools/e2b.py +121 -0
- mindroom/tools/eleven_labs.py +77 -0
- mindroom/tools/email.py +74 -0
- mindroom/tools/exa.py +246 -0
- mindroom/tools/fal.py +50 -0
- mindroom/tools/file.py +80 -0
- mindroom/tools/financial_datasets_api.py +112 -0
- mindroom/tools/firecrawl.py +124 -0
- mindroom/tools/gemini.py +85 -0
- mindroom/tools/giphy.py +49 -0
- mindroom/tools/github.py +376 -0
- mindroom/tools/gmail.py +102 -0
- mindroom/tools/google_calendar.py +55 -0
- mindroom/tools/google_maps.py +112 -0
- mindroom/tools/google_sheets.py +86 -0
- mindroom/tools/googlesearch.py +83 -0
- mindroom/tools/groq.py +77 -0
- mindroom/tools/hackernews.py +54 -0
- mindroom/tools/jina.py +108 -0
- mindroom/tools/jira.py +70 -0
- mindroom/tools/linear.py +103 -0
- mindroom/tools/linkup.py +65 -0
- mindroom/tools/lumalabs.py +71 -0
- mindroom/tools/mem0.py +82 -0
- mindroom/tools/modelslabs.py +85 -0
- mindroom/tools/moviepy_video_tools.py +62 -0
- mindroom/tools/newspaper4k.py +63 -0
- mindroom/tools/openai.py +143 -0
- mindroom/tools/openweather.py +89 -0
- mindroom/tools/oxylabs.py +54 -0
- mindroom/tools/pandas.py +35 -0
- mindroom/tools/pubmed.py +64 -0
- mindroom/tools/python.py +120 -0
- mindroom/tools/reddit.py +155 -0
- mindroom/tools/replicate.py +56 -0
- mindroom/tools/resend.py +55 -0
- mindroom/tools/scrapegraph.py +87 -0
- mindroom/tools/searxng.py +120 -0
- mindroom/tools/serpapi.py +55 -0
- mindroom/tools/serper.py +81 -0
- mindroom/tools/shell.py +46 -0
- mindroom/tools/slack.py +80 -0
- mindroom/tools/sleep.py +38 -0
- mindroom/tools/spider.py +62 -0
- mindroom/tools/sql.py +138 -0
- mindroom/tools/tavily.py +104 -0
- mindroom/tools/telegram.py +54 -0
- mindroom/tools/todoist.py +103 -0
- mindroom/tools/trello.py +121 -0
- mindroom/tools/twilio.py +97 -0
- mindroom/tools/web_browser_tools.py +37 -0
- mindroom/tools/webex.py +63 -0
- mindroom/tools/website.py +45 -0
- mindroom/tools/whatsapp.py +81 -0
- mindroom/tools/wikipedia.py +45 -0
- mindroom/tools/x.py +97 -0
- mindroom/tools/yfinance.py +121 -0
- mindroom/tools/youtube.py +81 -0
- mindroom/tools/zendesk.py +62 -0
- mindroom/tools/zep.py +107 -0
- mindroom/tools/zoom.py +62 -0
- mindroom/tools_metadata.json +7643 -0
- mindroom/tools_metadata.py +220 -0
- mindroom/topic_generator.py +153 -0
- mindroom/voice_handler.py +266 -0
- mindroom-0.1.1.dist-info/METADATA +425 -0
- mindroom-0.1.1.dist-info/RECORD +152 -0
- {mindroom-0.0.0.dist-info → mindroom-0.1.1.dist-info}/WHEEL +1 -2
- mindroom-0.1.1.dist-info/entry_points.txt +2 -0
- mindroom-0.0.0.dist-info/METADATA +0 -24
- mindroom-0.0.0.dist-info/RECORD +0 -4
- mindroom-0.0.0.dist-info/top_level.txt +0 -1
mindroom/config.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Pydantic models for configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from .constants import DEFAULT_AGENTS_CONFIG, MATRIX_HOMESERVER, ROUTER_AGENT_NAME
|
|
13
|
+
from .logging_config import get_logger
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .matrix.identity import MatrixID
|
|
17
|
+
|
|
18
|
+
logger = get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentConfig(BaseModel):
|
|
22
|
+
"""Configuration for a single agent."""
|
|
23
|
+
|
|
24
|
+
display_name: str = Field(description="Human-readable name for the agent")
|
|
25
|
+
role: str = Field(default="", description="Description of the agent's purpose")
|
|
26
|
+
tools: list[str] = Field(default_factory=list, description="List of tool names")
|
|
27
|
+
instructions: list[str] = Field(default_factory=list, description="Agent instructions")
|
|
28
|
+
rooms: list[str] = Field(default_factory=list, description="List of room IDs or names to auto-join")
|
|
29
|
+
num_history_runs: int | None = Field(default=None, description="Number of history runs to include")
|
|
30
|
+
markdown: bool | None = Field(default=None, description="Whether to use markdown formatting")
|
|
31
|
+
add_history_to_messages: bool | None = Field(default=None, description="Whether to add history to messages")
|
|
32
|
+
model: str = Field(default="default", description="Model name")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DefaultsConfig(BaseModel):
|
|
36
|
+
"""Default configuration values for agents."""
|
|
37
|
+
|
|
38
|
+
num_history_runs: int = Field(default=5, description="Default number of history runs")
|
|
39
|
+
markdown: bool = Field(default=True, description="Default markdown setting")
|
|
40
|
+
add_history_to_messages: bool = Field(default=True, description="Default history setting")
|
|
41
|
+
show_stop_button: bool = Field(default=False, description="Whether to automatically show stop button on messages")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EmbedderConfig(BaseModel):
|
|
45
|
+
"""Configuration for memory embedder."""
|
|
46
|
+
|
|
47
|
+
model: str = Field(default="text-embedding-3-small", description="Model name for embeddings")
|
|
48
|
+
api_key: str | None = Field(default=None, description="API key (usually from environment variable)")
|
|
49
|
+
host: str | None = Field(default=None, description="Host URL for self-hosted models like Ollama")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MemoryEmbedderConfig(BaseModel):
|
|
53
|
+
"""Memory embedder configuration."""
|
|
54
|
+
|
|
55
|
+
provider: str = Field(default="openai", description="Embedder provider (openai, huggingface, etc)")
|
|
56
|
+
config: EmbedderConfig = Field(default_factory=EmbedderConfig, description="Provider-specific config")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MemoryLLMConfig(BaseModel):
|
|
60
|
+
"""Memory LLM configuration."""
|
|
61
|
+
|
|
62
|
+
provider: str = Field(default="ollama", description="LLM provider (ollama, openai, anthropic)")
|
|
63
|
+
config: dict[str, Any] = Field(default_factory=dict, description="Provider-specific LLM config")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MemoryConfig(BaseModel):
|
|
67
|
+
"""Memory system configuration."""
|
|
68
|
+
|
|
69
|
+
embedder: MemoryEmbedderConfig = Field(
|
|
70
|
+
default_factory=MemoryEmbedderConfig,
|
|
71
|
+
description="Embedder configuration for memory",
|
|
72
|
+
)
|
|
73
|
+
llm: MemoryLLMConfig | None = Field(default=None, description="LLM configuration for memory")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ModelConfig(BaseModel):
|
|
77
|
+
"""Configuration for an AI model."""
|
|
78
|
+
|
|
79
|
+
provider: str = Field(description="Model provider (openai, anthropic, ollama, etc)")
|
|
80
|
+
id: str = Field(description="Model ID specific to the provider")
|
|
81
|
+
host: str | None = Field(default=None, description="Optional host URL (e.g., for Ollama)")
|
|
82
|
+
api_key: str | None = Field(default=None, description="Optional API key (usually from env vars)")
|
|
83
|
+
extra_kwargs: dict[str, Any] | None = Field(
|
|
84
|
+
default=None,
|
|
85
|
+
description="Additional provider-specific parameters passed directly to the model",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RouterConfig(BaseModel):
|
|
90
|
+
"""Configuration for the router system."""
|
|
91
|
+
|
|
92
|
+
model: str = Field(default="default", description="Model to use for routing decisions")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TeamConfig(BaseModel):
|
|
96
|
+
"""Configuration for a team of agents."""
|
|
97
|
+
|
|
98
|
+
display_name: str = Field(description="Human-readable name for the team")
|
|
99
|
+
role: str = Field(description="Description of the team's purpose")
|
|
100
|
+
agents: list[str] = Field(description="List of agent names that compose this team")
|
|
101
|
+
rooms: list[str] = Field(default_factory=list, description="List of room IDs or names to auto-join")
|
|
102
|
+
model: str | None = Field(default="default", description="Default model for this team (optional)")
|
|
103
|
+
mode: str = Field(default="coordinate", description="Team collaboration mode: coordinate or collaborate")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class VoiceSTTConfig(BaseModel):
|
|
107
|
+
"""Configuration for voice speech-to-text."""
|
|
108
|
+
|
|
109
|
+
provider: str = Field(default="openai", description="STT provider (openai or compatible)")
|
|
110
|
+
model: str = Field(default="whisper-1", description="STT model name")
|
|
111
|
+
api_key: str | None = Field(default=None, description="API key for STT service")
|
|
112
|
+
host: str | None = Field(default=None, description="Host URL for self-hosted STT")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class VoiceLLMConfig(BaseModel):
|
|
116
|
+
"""Configuration for voice command intelligence."""
|
|
117
|
+
|
|
118
|
+
model: str = Field(default="default", description="Model for command recognition")
|
|
119
|
+
confidence_threshold: float = Field(default=0.7, description="Confidence threshold for commands")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class VoiceConfig(BaseModel):
|
|
123
|
+
"""Configuration for voice message handling."""
|
|
124
|
+
|
|
125
|
+
enabled: bool = Field(default=False, description="Enable voice message processing")
|
|
126
|
+
stt: VoiceSTTConfig = Field(default_factory=VoiceSTTConfig, description="STT configuration")
|
|
127
|
+
intelligence: VoiceLLMConfig = Field(
|
|
128
|
+
default_factory=VoiceLLMConfig,
|
|
129
|
+
description="Command intelligence configuration",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AuthorizationConfig(BaseModel):
|
|
134
|
+
"""Authorization configuration with fine-grained permissions."""
|
|
135
|
+
|
|
136
|
+
global_users: list[str] = Field(
|
|
137
|
+
default_factory=list,
|
|
138
|
+
description="Users with access to all rooms (e.g., '@user:example.com')",
|
|
139
|
+
)
|
|
140
|
+
room_permissions: dict[str, list[str]] = Field(
|
|
141
|
+
default_factory=dict,
|
|
142
|
+
description="Room-specific user permissions. Keys are room IDs, values are lists of authorized user IDs",
|
|
143
|
+
)
|
|
144
|
+
default_room_access: bool = Field(
|
|
145
|
+
default=False,
|
|
146
|
+
description="Default permission for rooms not explicitly configured",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Config(BaseModel):
|
|
151
|
+
"""Complete configuration from YAML."""
|
|
152
|
+
|
|
153
|
+
agents: dict[str, AgentConfig] = Field(default_factory=dict, description="Agent configurations")
|
|
154
|
+
teams: dict[str, TeamConfig] = Field(default_factory=dict, description="Team configurations")
|
|
155
|
+
room_models: dict[str, str] = Field(default_factory=dict, description="Room-specific model overrides")
|
|
156
|
+
defaults: DefaultsConfig = Field(default_factory=DefaultsConfig, description="Default values")
|
|
157
|
+
memory: MemoryConfig = Field(default_factory=MemoryConfig, description="Memory configuration")
|
|
158
|
+
models: dict[str, ModelConfig] = Field(default_factory=dict, description="Model configurations")
|
|
159
|
+
router: RouterConfig = Field(default_factory=RouterConfig, description="Router configuration")
|
|
160
|
+
voice: VoiceConfig = Field(default_factory=VoiceConfig, description="Voice configuration")
|
|
161
|
+
timezone: str = Field(
|
|
162
|
+
default="UTC",
|
|
163
|
+
description="Timezone for displaying scheduled tasks (e.g., 'America/New_York')",
|
|
164
|
+
)
|
|
165
|
+
authorization: AuthorizationConfig = Field(
|
|
166
|
+
default_factory=AuthorizationConfig,
|
|
167
|
+
description="Authorization configuration with fine-grained permissions",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@cached_property
|
|
171
|
+
def domain(self) -> str:
|
|
172
|
+
"""Extract the domain from the MATRIX_HOMESERVER."""
|
|
173
|
+
from .matrix.identity import extract_server_name_from_homeserver # noqa: PLC0415
|
|
174
|
+
|
|
175
|
+
return extract_server_name_from_homeserver(MATRIX_HOMESERVER)
|
|
176
|
+
|
|
177
|
+
@cached_property
|
|
178
|
+
def ids(self) -> dict[str, MatrixID]:
|
|
179
|
+
"""Get MatrixID objects for all agents and teams.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dictionary mapping agent/team names to their MatrixID objects.
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
from .matrix.identity import MatrixID # noqa: PLC0415
|
|
186
|
+
|
|
187
|
+
mapping: dict[str, MatrixID] = {}
|
|
188
|
+
|
|
189
|
+
# Add all agents
|
|
190
|
+
for agent_name in self.agents:
|
|
191
|
+
mapping[agent_name] = MatrixID.from_agent(agent_name, self.domain)
|
|
192
|
+
|
|
193
|
+
# Add router agent separately (it's not in config.agents)
|
|
194
|
+
mapping[ROUTER_AGENT_NAME] = MatrixID.from_agent(ROUTER_AGENT_NAME, self.domain)
|
|
195
|
+
|
|
196
|
+
# Add all teams
|
|
197
|
+
for team_name in self.teams:
|
|
198
|
+
mapping[team_name] = MatrixID.from_agent(team_name, self.domain)
|
|
199
|
+
return mapping
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_yaml(cls, config_path: Path | None = None) -> Config:
|
|
203
|
+
"""Create a Config instance from YAML data."""
|
|
204
|
+
path = config_path or DEFAULT_AGENTS_CONFIG
|
|
205
|
+
|
|
206
|
+
if not path.exists():
|
|
207
|
+
msg = f"Agent configuration file not found: {path}"
|
|
208
|
+
raise FileNotFoundError(msg)
|
|
209
|
+
|
|
210
|
+
with path.open() as f:
|
|
211
|
+
data = yaml.safe_load(f)
|
|
212
|
+
|
|
213
|
+
# Handle None values for optional dictionaries
|
|
214
|
+
if data.get("teams") is None:
|
|
215
|
+
data["teams"] = {}
|
|
216
|
+
if data.get("room_models") is None:
|
|
217
|
+
data["room_models"] = {}
|
|
218
|
+
|
|
219
|
+
config = cls(**data)
|
|
220
|
+
logger.info(f"Loaded agent configuration from {path}")
|
|
221
|
+
logger.info(f"Found {len(config.agents)} agent configurations")
|
|
222
|
+
return config
|
|
223
|
+
|
|
224
|
+
def get_agent(self, agent_name: str) -> AgentConfig:
|
|
225
|
+
"""Get an agent configuration by name.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
agent_name: Name of the agent
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Agent configuration
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
ValueError: If agent not found
|
|
235
|
+
|
|
236
|
+
"""
|
|
237
|
+
if agent_name not in self.agents:
|
|
238
|
+
available = ", ".join(sorted(self.agents.keys()))
|
|
239
|
+
msg = f"Unknown agent: {agent_name}. Available agents: {available}"
|
|
240
|
+
raise ValueError(msg)
|
|
241
|
+
return self.agents[agent_name]
|
|
242
|
+
|
|
243
|
+
def get_all_configured_rooms(self) -> set[str]:
|
|
244
|
+
"""Extract all room aliases configured for agents and teams.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Set of all unique room aliases from agent and team configurations
|
|
248
|
+
|
|
249
|
+
"""
|
|
250
|
+
all_room_aliases = set()
|
|
251
|
+
for agent_config in self.agents.values():
|
|
252
|
+
all_room_aliases.update(agent_config.rooms)
|
|
253
|
+
for team_config in self.teams.values():
|
|
254
|
+
all_room_aliases.update(team_config.rooms)
|
|
255
|
+
return all_room_aliases
|
|
256
|
+
|
|
257
|
+
def get_entity_model_name(self, entity_name: str) -> str:
|
|
258
|
+
"""Get the model name for an agent, team, or router.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
entity_name: Name of the entity (agent, team, or router)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Model name (e.g., "default", "gpt-4", etc.)
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: If entity_name is not found in configuration
|
|
268
|
+
|
|
269
|
+
"""
|
|
270
|
+
# Router uses router model
|
|
271
|
+
if entity_name == ROUTER_AGENT_NAME:
|
|
272
|
+
return self.router.model
|
|
273
|
+
# Teams use their configured model (required to have one)
|
|
274
|
+
if entity_name in self.teams:
|
|
275
|
+
model = self.teams[entity_name].model
|
|
276
|
+
if model is None:
|
|
277
|
+
msg = f"Team {entity_name} has no model configured"
|
|
278
|
+
raise ValueError(msg)
|
|
279
|
+
return model
|
|
280
|
+
# Regular agents use their configured model
|
|
281
|
+
if entity_name in self.agents:
|
|
282
|
+
return self.agents[entity_name].model
|
|
283
|
+
|
|
284
|
+
# Entity not found in any category
|
|
285
|
+
available = sorted(set(self.agents.keys()) | set(self.teams.keys()) | {ROUTER_AGENT_NAME})
|
|
286
|
+
msg = f"Unknown entity: {entity_name}. Available entities: {', '.join(available)}"
|
|
287
|
+
raise ValueError(msg)
|
|
288
|
+
|
|
289
|
+
def get_configured_bots_for_room(self, room_id: str) -> set[str]:
|
|
290
|
+
"""Get the set of bot usernames that should be in a specific room.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
room_id: The Matrix room ID
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
Set of bot usernames (without domain) that should be in this room
|
|
297
|
+
|
|
298
|
+
"""
|
|
299
|
+
from .matrix.rooms import resolve_room_aliases # noqa: PLC0415
|
|
300
|
+
|
|
301
|
+
configured_bots = set()
|
|
302
|
+
|
|
303
|
+
# Check which agents should be in this room
|
|
304
|
+
for agent_name, agent_config in self.agents.items():
|
|
305
|
+
resolved_rooms = set(resolve_room_aliases(agent_config.rooms))
|
|
306
|
+
if room_id in resolved_rooms:
|
|
307
|
+
configured_bots.add(f"mindroom_{agent_name}")
|
|
308
|
+
|
|
309
|
+
# Check which teams should be in this room
|
|
310
|
+
for team_name, team_config in self.teams.items():
|
|
311
|
+
resolved_rooms = set(resolve_room_aliases(team_config.rooms))
|
|
312
|
+
if room_id in resolved_rooms:
|
|
313
|
+
configured_bots.add(f"mindroom_{team_name}")
|
|
314
|
+
|
|
315
|
+
# Router should be in any room that has any configured agents/teams
|
|
316
|
+
if configured_bots: # If any bots are configured for this room
|
|
317
|
+
configured_bots.add(f"mindroom_{ROUTER_AGENT_NAME}")
|
|
318
|
+
|
|
319
|
+
return configured_bots
|
|
320
|
+
|
|
321
|
+
def save_to_yaml(self, config_path: Path | None = None) -> None:
|
|
322
|
+
"""Save the config to a YAML file, excluding None values.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
config_path: Path to save the config to. If None, uses DEFAULT_AGENTS_CONFIG.
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
path = config_path or DEFAULT_AGENTS_CONFIG
|
|
329
|
+
config_dict = self.model_dump(exclude_none=True)
|
|
330
|
+
path_obj = Path(path)
|
|
331
|
+
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
332
|
+
tmp_path = path_obj.with_suffix(path_obj.suffix + ".tmp")
|
|
333
|
+
with tmp_path.open("w", encoding="utf-8") as f:
|
|
334
|
+
yaml.dump(
|
|
335
|
+
config_dict,
|
|
336
|
+
f,
|
|
337
|
+
default_flow_style=False,
|
|
338
|
+
sort_keys=True,
|
|
339
|
+
allow_unicode=True, # Preserve Unicode characters like ë
|
|
340
|
+
width=120, # Wider lines to reduce wrapping
|
|
341
|
+
)
|
|
342
|
+
tmp_path.replace(path_obj)
|
|
343
|
+
logger.info(f"Saved configuration to {path}")
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Configuration command handling for user-driven config changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
from pathlib import Path # noqa: TC003
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from .config import Config
|
|
13
|
+
from .constants import DEFAULT_AGENTS_CONFIG
|
|
14
|
+
from .logging_config import get_logger
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_config_args(args_text: str) -> tuple[str, list[str]]:
|
|
20
|
+
"""Parse config command arguments.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
args_text: Raw argument text from command
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (operation, arguments)
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
if not args_text:
|
|
30
|
+
return "show", []
|
|
31
|
+
|
|
32
|
+
# Use shlex to handle quoted strings properly
|
|
33
|
+
try:
|
|
34
|
+
parts = shlex.split(args_text)
|
|
35
|
+
except ValueError as e:
|
|
36
|
+
# Handle parsing errors (e.g., unmatched quotes)
|
|
37
|
+
# Return a special operation that will trigger an error message
|
|
38
|
+
return "parse_error", [str(e)]
|
|
39
|
+
|
|
40
|
+
if not parts:
|
|
41
|
+
return "show", []
|
|
42
|
+
|
|
43
|
+
operation = parts[0].lower()
|
|
44
|
+
args = parts[1:] if len(parts) > 1 else []
|
|
45
|
+
return operation, args
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_nested_value(data: Any, path: str) -> Any: # noqa: ANN401
|
|
49
|
+
"""Get a value from nested dict using dot notation.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data: The dictionary to search
|
|
53
|
+
path: Dot-separated path (e.g., "agents.analyst.display_name")
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The value at the path
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
KeyError: If path doesn't exist
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
keys = path.split(".")
|
|
63
|
+
current = data
|
|
64
|
+
|
|
65
|
+
for key in keys:
|
|
66
|
+
# Handle array indexing
|
|
67
|
+
if key.isdigit(): # noqa: SIM108
|
|
68
|
+
current = current[int(key)]
|
|
69
|
+
else:
|
|
70
|
+
current = current[key]
|
|
71
|
+
|
|
72
|
+
return current
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_nested_value(data: Any, path: str, value: Any) -> None: # noqa: ANN401
|
|
76
|
+
"""Set a value in nested dict using dot notation.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
data: The dictionary to modify
|
|
80
|
+
path: Dot-separated path (e.g., "agents.analyst.display_name")
|
|
81
|
+
value: Value to set
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
KeyError: If parent path doesn't exist
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
keys = path.split(".")
|
|
88
|
+
current = data
|
|
89
|
+
|
|
90
|
+
# Navigate to the parent of the target
|
|
91
|
+
for key in keys[:-1]:
|
|
92
|
+
if key.isdigit():
|
|
93
|
+
current = current[int(key)]
|
|
94
|
+
elif key not in current:
|
|
95
|
+
# Auto-create missing intermediate dicts
|
|
96
|
+
current[key] = {}
|
|
97
|
+
current = current[key]
|
|
98
|
+
else:
|
|
99
|
+
current = current[key]
|
|
100
|
+
|
|
101
|
+
# Set the final value
|
|
102
|
+
final_key = keys[-1]
|
|
103
|
+
if final_key.isdigit():
|
|
104
|
+
current[int(final_key)] = value
|
|
105
|
+
else:
|
|
106
|
+
current[final_key] = value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def parse_value(value_str: str) -> Any: # noqa: ANN401
|
|
110
|
+
"""Parse a string value into appropriate Python type.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
value_str: String representation of value
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Parsed value (str, int, float, bool, list, or dict)
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
# Try to parse as YAML first (handles unquoted strings in arrays/dicts)
|
|
120
|
+
# YAML is a superset of JSON, so this handles both formats
|
|
121
|
+
# Examples that work:
|
|
122
|
+
# [item1, item2] -> ['item1', 'item2']
|
|
123
|
+
# ["item1", "item2"] -> ['item1', 'item2']
|
|
124
|
+
# {key: value} -> {'key': 'value'}
|
|
125
|
+
# {"key": "value"} -> {'key': 'value'}
|
|
126
|
+
try:
|
|
127
|
+
return yaml.safe_load(value_str)
|
|
128
|
+
except yaml.YAMLError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# If YAML parsing fails, return as string
|
|
132
|
+
# This handles cases where the string itself contains special YAML characters
|
|
133
|
+
return value_str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def format_value(value: Any) -> str: # noqa: ANN401
|
|
137
|
+
"""Format a value for display as YAML.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
value: Value to format
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
YAML formatted string representation
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
# Use yaml.dump for consistent formatting
|
|
147
|
+
yaml_str = yaml.dump(value, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
148
|
+
# Remove trailing newline and document end marker that yaml.dump adds
|
|
149
|
+
yaml_str = yaml_str.rstrip()
|
|
150
|
+
if yaml_str.endswith("..."):
|
|
151
|
+
yaml_str = yaml_str[:-3].rstrip()
|
|
152
|
+
return yaml_str
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def handle_config_command(args_text: str, config_path: Path | None = None) -> tuple[str, dict[str, Any] | None]: # noqa: C901, PLR0911, PLR0912
|
|
156
|
+
"""Handle config command execution.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
args_text: The command arguments
|
|
160
|
+
config_path: Optional path to config file
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Tuple of (response message, config change dict or None)
|
|
164
|
+
The config change dict contains info needed for confirmation
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
operation, args = parse_config_args(args_text)
|
|
168
|
+
path = config_path or DEFAULT_AGENTS_CONFIG
|
|
169
|
+
|
|
170
|
+
# Load current config
|
|
171
|
+
config = Config.from_yaml(path)
|
|
172
|
+
config_dict = config.model_dump(exclude_none=True)
|
|
173
|
+
|
|
174
|
+
if operation == "show":
|
|
175
|
+
# Show entire config
|
|
176
|
+
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
177
|
+
return f"**Current Configuration:**\n```yaml\n{yaml_str}```", None
|
|
178
|
+
|
|
179
|
+
if operation == "get":
|
|
180
|
+
if not args:
|
|
181
|
+
return (
|
|
182
|
+
"❌ Please specify a configuration path to get\nExample: `!config get agents.analyst.display_name`",
|
|
183
|
+
None,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
config_path_str = args[0]
|
|
187
|
+
try:
|
|
188
|
+
value = get_nested_value(config_dict, config_path_str)
|
|
189
|
+
except (KeyError, IndexError) as e:
|
|
190
|
+
return f"❌ Configuration path not found: `{config_path_str}`\nError: {e}", None
|
|
191
|
+
else:
|
|
192
|
+
formatted = format_value(value)
|
|
193
|
+
return f"**Configuration value for `{config_path_str}`:**\n```yaml\n{formatted}\n```", None
|
|
194
|
+
|
|
195
|
+
elif operation == "set":
|
|
196
|
+
if len(args) < 2:
|
|
197
|
+
return (
|
|
198
|
+
'❌ Please specify a path and value\nExample: `!config set agents.analyst.display_name "New Name"`',
|
|
199
|
+
None,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
config_path_str = args[0]
|
|
203
|
+
# Join remaining args as the value (handles unquoted strings with spaces)
|
|
204
|
+
value_str = " ".join(args[1:])
|
|
205
|
+
|
|
206
|
+
# Parse the value - YAML parsing handles both quoted and unquoted formats
|
|
207
|
+
value = parse_value(value_str)
|
|
208
|
+
|
|
209
|
+
# Get the current value for comparison
|
|
210
|
+
try:
|
|
211
|
+
old_value = get_nested_value(config_dict, config_path_str)
|
|
212
|
+
except (KeyError, IndexError):
|
|
213
|
+
old_value = None # Path doesn't exist yet
|
|
214
|
+
|
|
215
|
+
# Create a copy to test the change
|
|
216
|
+
test_config_dict = config_dict.copy()
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
# Verify the path exists or can be created
|
|
220
|
+
set_nested_value(test_config_dict, config_path_str, value)
|
|
221
|
+
|
|
222
|
+
# Validate the modified config
|
|
223
|
+
Config(**test_config_dict) # This will raise ValidationError if invalid
|
|
224
|
+
except (KeyError, IndexError) as e:
|
|
225
|
+
return f"❌ Configuration path error: `{config_path_str}`\nError: {e}", None
|
|
226
|
+
except ValidationError as e:
|
|
227
|
+
# Validation failed - explain why
|
|
228
|
+
errors = []
|
|
229
|
+
for error in e.errors():
|
|
230
|
+
location = " → ".join(str(loc) for loc in error["loc"])
|
|
231
|
+
errors.append(f"• {location}: {error['msg']}")
|
|
232
|
+
error_msg = "\n".join(errors)
|
|
233
|
+
return f"❌ Invalid configuration:\n{error_msg}\n\nChanges were NOT applied.", None
|
|
234
|
+
else:
|
|
235
|
+
# Format the preview message
|
|
236
|
+
formatted_old = format_value(old_value) if old_value is not None else "Not set"
|
|
237
|
+
formatted_new = format_value(value)
|
|
238
|
+
|
|
239
|
+
preview_msg = (
|
|
240
|
+
f"**Configuration Change Preview**\n\n"
|
|
241
|
+
f"📝 **Path:** `{config_path_str}`\n\n"
|
|
242
|
+
f"**Current value:**\n```yaml\n{formatted_old}\n```\n"
|
|
243
|
+
f"**New value:**\n```yaml\n{formatted_new}\n```\n\n"
|
|
244
|
+
f"React with ✅ to confirm or ❌ to cancel this change."
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Return the preview and the change info for confirmation
|
|
248
|
+
change_info = {
|
|
249
|
+
"config_path": config_path_str,
|
|
250
|
+
"old_value": old_value,
|
|
251
|
+
"new_value": value,
|
|
252
|
+
"path": str(path),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return preview_msg, change_info
|
|
256
|
+
|
|
257
|
+
elif operation == "parse_error":
|
|
258
|
+
# Handle parsing errors (e.g., unmatched quotes)
|
|
259
|
+
error_msg = args[0] if args else "Unknown parsing error"
|
|
260
|
+
return (
|
|
261
|
+
f"❌ **Command parsing error:**\n{error_msg}\n\n"
|
|
262
|
+
"**Common issues:**\n"
|
|
263
|
+
"• Unmatched quotes: Make sure quotes are properly paired\n"
|
|
264
|
+
'• For JSON arrays/objects, use matching quotes: `["item1", "item2"]`\n'
|
|
265
|
+
"• Or use single quotes consistently: `['item1', 'item2']`\n\n"
|
|
266
|
+
"**Example:**\n"
|
|
267
|
+
'`!config set agents.analyst.tools ["tool1", "tool2"]`'
|
|
268
|
+
), None
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
available_ops = ["show", "get", "set"]
|
|
272
|
+
return (
|
|
273
|
+
f"❌ Unknown operation: '{operation}'\n"
|
|
274
|
+
f"Available operations: {', '.join(available_ops)}\n\n"
|
|
275
|
+
"Try `!help config` for usage examples."
|
|
276
|
+
), None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def apply_config_change(
|
|
280
|
+
config_path_str: str,
|
|
281
|
+
new_value: Any, # noqa: ANN401
|
|
282
|
+
config_file_path: Path | None = None,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""Apply a confirmed configuration change.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
config_path_str: The configuration path (e.g., "agents.analyst.role")
|
|
288
|
+
new_value: The new value to set
|
|
289
|
+
config_file_path: Optional path to config file
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Success or error message
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
path = config_file_path or DEFAULT_AGENTS_CONFIG
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
# Load the current configuration
|
|
299
|
+
config = Config.from_yaml(path)
|
|
300
|
+
config_dict = config.model_dump()
|
|
301
|
+
|
|
302
|
+
# Apply the specific change
|
|
303
|
+
set_nested_value(config_dict, config_path_str, new_value)
|
|
304
|
+
|
|
305
|
+
# Validate the modified config
|
|
306
|
+
try:
|
|
307
|
+
new_config = Config(**config_dict)
|
|
308
|
+
except ValidationError as ve:
|
|
309
|
+
errors = ["❌ Configuration validation failed:"]
|
|
310
|
+
for error in ve.errors():
|
|
311
|
+
location = " → ".join(str(loc) for loc in error["loc"])
|
|
312
|
+
errors.append(f"• {location}: {error['msg']}")
|
|
313
|
+
error_msg = "\n".join(errors)
|
|
314
|
+
return f"{error_msg}\n\nChanges were NOT applied."
|
|
315
|
+
|
|
316
|
+
# Save to file
|
|
317
|
+
new_config.save_to_yaml(path)
|
|
318
|
+
return ( # noqa: TRY300
|
|
319
|
+
f"✅ **Configuration updated successfully!**\n\n"
|
|
320
|
+
f"Changes saved to {path} and will affect new agent interactions."
|
|
321
|
+
)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
logger.exception("Failed to apply config change")
|
|
324
|
+
return f"❌ Failed to apply configuration change: {e}"
|