mindroom 0.0.0__py3-none-any.whl → 0.1.0__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 (155) hide show
  1. mindroom/__init__.py +3 -0
  2. mindroom/agent_prompts.py +963 -0
  3. mindroom/agents.py +248 -0
  4. mindroom/ai.py +421 -0
  5. mindroom/api/__init__.py +1 -0
  6. mindroom/api/credentials.py +137 -0
  7. mindroom/api/google_integration.py +355 -0
  8. mindroom/api/google_tools_helper.py +40 -0
  9. mindroom/api/homeassistant_integration.py +421 -0
  10. mindroom/api/integrations.py +189 -0
  11. mindroom/api/main.py +506 -0
  12. mindroom/api/matrix_operations.py +219 -0
  13. mindroom/api/tools.py +94 -0
  14. mindroom/background_tasks.py +87 -0
  15. mindroom/bot.py +2470 -0
  16. mindroom/cli.py +86 -0
  17. mindroom/commands.py +377 -0
  18. mindroom/config.py +343 -0
  19. mindroom/config_commands.py +324 -0
  20. mindroom/config_confirmation.py +411 -0
  21. mindroom/constants.py +52 -0
  22. mindroom/credentials.py +146 -0
  23. mindroom/credentials_sync.py +134 -0
  24. mindroom/custom_tools/__init__.py +8 -0
  25. mindroom/custom_tools/config_manager.py +765 -0
  26. mindroom/custom_tools/gmail.py +92 -0
  27. mindroom/custom_tools/google_calendar.py +92 -0
  28. mindroom/custom_tools/google_sheets.py +92 -0
  29. mindroom/custom_tools/homeassistant.py +341 -0
  30. mindroom/error_handling.py +35 -0
  31. mindroom/file_watcher.py +49 -0
  32. mindroom/interactive.py +313 -0
  33. mindroom/logging_config.py +207 -0
  34. mindroom/matrix/__init__.py +1 -0
  35. mindroom/matrix/client.py +782 -0
  36. mindroom/matrix/event_info.py +173 -0
  37. mindroom/matrix/identity.py +149 -0
  38. mindroom/matrix/large_messages.py +267 -0
  39. mindroom/matrix/mentions.py +141 -0
  40. mindroom/matrix/message_builder.py +94 -0
  41. mindroom/matrix/message_content.py +209 -0
  42. mindroom/matrix/presence.py +178 -0
  43. mindroom/matrix/rooms.py +311 -0
  44. mindroom/matrix/state.py +77 -0
  45. mindroom/matrix/typing.py +91 -0
  46. mindroom/matrix/users.py +217 -0
  47. mindroom/memory/__init__.py +21 -0
  48. mindroom/memory/config.py +137 -0
  49. mindroom/memory/functions.py +396 -0
  50. mindroom/py.typed +0 -0
  51. mindroom/response_tracker.py +128 -0
  52. mindroom/room_cleanup.py +139 -0
  53. mindroom/routing.py +107 -0
  54. mindroom/scheduling.py +758 -0
  55. mindroom/stop.py +207 -0
  56. mindroom/streaming.py +203 -0
  57. mindroom/teams.py +749 -0
  58. mindroom/thread_utils.py +318 -0
  59. mindroom/tools/__init__.py +520 -0
  60. mindroom/tools/agentql.py +64 -0
  61. mindroom/tools/airflow.py +57 -0
  62. mindroom/tools/apify.py +49 -0
  63. mindroom/tools/arxiv.py +64 -0
  64. mindroom/tools/aws_lambda.py +41 -0
  65. mindroom/tools/aws_ses.py +57 -0
  66. mindroom/tools/baidusearch.py +87 -0
  67. mindroom/tools/brightdata.py +116 -0
  68. mindroom/tools/browserbase.py +62 -0
  69. mindroom/tools/cal_com.py +98 -0
  70. mindroom/tools/calculator.py +112 -0
  71. mindroom/tools/cartesia.py +84 -0
  72. mindroom/tools/composio.py +166 -0
  73. mindroom/tools/config_manager.py +44 -0
  74. mindroom/tools/confluence.py +73 -0
  75. mindroom/tools/crawl4ai.py +101 -0
  76. mindroom/tools/csv.py +104 -0
  77. mindroom/tools/custom_api.py +106 -0
  78. mindroom/tools/dalle.py +85 -0
  79. mindroom/tools/daytona.py +180 -0
  80. mindroom/tools/discord.py +81 -0
  81. mindroom/tools/docker.py +73 -0
  82. mindroom/tools/duckdb.py +124 -0
  83. mindroom/tools/duckduckgo.py +99 -0
  84. mindroom/tools/e2b.py +121 -0
  85. mindroom/tools/eleven_labs.py +77 -0
  86. mindroom/tools/email.py +74 -0
  87. mindroom/tools/exa.py +246 -0
  88. mindroom/tools/fal.py +50 -0
  89. mindroom/tools/file.py +80 -0
  90. mindroom/tools/financial_datasets_api.py +112 -0
  91. mindroom/tools/firecrawl.py +124 -0
  92. mindroom/tools/gemini.py +85 -0
  93. mindroom/tools/giphy.py +49 -0
  94. mindroom/tools/github.py +376 -0
  95. mindroom/tools/gmail.py +102 -0
  96. mindroom/tools/google_calendar.py +55 -0
  97. mindroom/tools/google_maps.py +112 -0
  98. mindroom/tools/google_sheets.py +86 -0
  99. mindroom/tools/googlesearch.py +83 -0
  100. mindroom/tools/groq.py +77 -0
  101. mindroom/tools/hackernews.py +54 -0
  102. mindroom/tools/jina.py +108 -0
  103. mindroom/tools/jira.py +70 -0
  104. mindroom/tools/linear.py +103 -0
  105. mindroom/tools/linkup.py +65 -0
  106. mindroom/tools/lumalabs.py +71 -0
  107. mindroom/tools/mem0.py +82 -0
  108. mindroom/tools/modelslabs.py +85 -0
  109. mindroom/tools/moviepy_video_tools.py +62 -0
  110. mindroom/tools/newspaper4k.py +63 -0
  111. mindroom/tools/openai.py +143 -0
  112. mindroom/tools/openweather.py +89 -0
  113. mindroom/tools/oxylabs.py +54 -0
  114. mindroom/tools/pandas.py +35 -0
  115. mindroom/tools/pubmed.py +64 -0
  116. mindroom/tools/python.py +120 -0
  117. mindroom/tools/reddit.py +155 -0
  118. mindroom/tools/replicate.py +56 -0
  119. mindroom/tools/resend.py +55 -0
  120. mindroom/tools/scrapegraph.py +87 -0
  121. mindroom/tools/searxng.py +120 -0
  122. mindroom/tools/serpapi.py +55 -0
  123. mindroom/tools/serper.py +81 -0
  124. mindroom/tools/shell.py +46 -0
  125. mindroom/tools/slack.py +80 -0
  126. mindroom/tools/sleep.py +38 -0
  127. mindroom/tools/spider.py +62 -0
  128. mindroom/tools/sql.py +138 -0
  129. mindroom/tools/tavily.py +104 -0
  130. mindroom/tools/telegram.py +54 -0
  131. mindroom/tools/todoist.py +103 -0
  132. mindroom/tools/trello.py +121 -0
  133. mindroom/tools/twilio.py +97 -0
  134. mindroom/tools/web_browser_tools.py +37 -0
  135. mindroom/tools/webex.py +63 -0
  136. mindroom/tools/website.py +45 -0
  137. mindroom/tools/whatsapp.py +81 -0
  138. mindroom/tools/wikipedia.py +45 -0
  139. mindroom/tools/x.py +97 -0
  140. mindroom/tools/yfinance.py +121 -0
  141. mindroom/tools/youtube.py +81 -0
  142. mindroom/tools/zendesk.py +62 -0
  143. mindroom/tools/zep.py +107 -0
  144. mindroom/tools/zoom.py +62 -0
  145. mindroom/tools_metadata.json +7643 -0
  146. mindroom/tools_metadata.py +220 -0
  147. mindroom/topic_generator.py +153 -0
  148. mindroom/voice_handler.py +266 -0
  149. mindroom-0.1.0.dist-info/METADATA +425 -0
  150. mindroom-0.1.0.dist-info/RECORD +152 -0
  151. {mindroom-0.0.0.dist-info → mindroom-0.1.0.dist-info}/WHEEL +1 -2
  152. mindroom-0.1.0.dist-info/entry_points.txt +2 -0
  153. mindroom-0.0.0.dist-info/METADATA +0 -24
  154. mindroom-0.0.0.dist-info/RECORD +0 -4
  155. mindroom-0.0.0.dist-info/top_level.txt +0 -1
mindroom/api/main.py ADDED
@@ -0,0 +1,506 @@
1
+ # ruff: noqa: D100
2
+ import os
3
+ import shutil
4
+ import threading
5
+ from pathlib import Path
6
+ from typing import Annotated, Any
7
+
8
+ import yaml
9
+ from dotenv import load_dotenv
10
+ from fastapi import Depends, FastAPI, Header, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel
13
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
14
+ from watchdog.observers import Observer
15
+
16
+ # Import routers
17
+ from mindroom.api.credentials import router as credentials_router
18
+ from mindroom.api.google_integration import router as google_router
19
+ from mindroom.api.homeassistant_integration import router as homeassistant_router
20
+ from mindroom.api.integrations import router as integrations_router
21
+ from mindroom.api.matrix_operations import router as matrix_router
22
+ from mindroom.api.tools import router as tools_router
23
+ from mindroom.config import Config
24
+ from mindroom.constants import DEFAULT_AGENTS_CONFIG, DEFAULT_CONFIG_TEMPLATE
25
+ from mindroom.credentials_sync import sync_env_to_credentials
26
+
27
+ # Load environment variables from .env file
28
+ # Look for .env in the widget directory (parent of backend)
29
+ env_path = Path(__file__).parent.parent.parent / ".env"
30
+ load_dotenv(env_path)
31
+
32
+ app = FastAPI(title="MindRoom Widget Backend")
33
+
34
+ # Configure CORS for widget - allow multiple origins including port forwarding
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=[
38
+ "http://localhost:3003", # Frontend dev server alternative port
39
+ "http://localhost:5173", # Vite dev server default
40
+ "http://127.0.0.1:3003", # Alternative localhost
41
+ "http://127.0.0.1:5173",
42
+ "*", # Allow all origins for development (remove in production)
43
+ ],
44
+ allow_credentials=True,
45
+ allow_methods=["*"],
46
+ allow_headers=["*"],
47
+ expose_headers=["*"],
48
+ )
49
+
50
+ # Resolve configurable config paths
51
+ CONFIG_PATH = DEFAULT_AGENTS_CONFIG
52
+ CONFIG_TEMPLATE_PATH = DEFAULT_CONFIG_TEMPLATE
53
+
54
+
55
+ def ensure_writable_config() -> None:
56
+ """Ensure the config file exists at a writable location.
57
+
58
+ In managed deployments the writable config is placed on a persistent
59
+ volume while a read-only template is mounted separately. When the final
60
+ config file is missing we seed it from the template so both the bot and
61
+ API read/write the same path.
62
+ """
63
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
64
+
65
+ if CONFIG_PATH.exists():
66
+ return
67
+
68
+ if CONFIG_TEMPLATE_PATH != CONFIG_PATH and CONFIG_TEMPLATE_PATH.exists():
69
+ shutil.copyfile(CONFIG_TEMPLATE_PATH, CONFIG_PATH)
70
+ CONFIG_PATH.chmod(0o600)
71
+ print(f"Seeded config from template {CONFIG_TEMPLATE_PATH} -> {CONFIG_PATH}")
72
+ return
73
+
74
+ # Fallback: create a minimal valid YAML structure so initial loads succeed
75
+ CONFIG_PATH.write_text("agents: {}\nmodels: {}\n", encoding="utf-8")
76
+ CONFIG_PATH.chmod(0o600)
77
+ print(f"Created new config file at {CONFIG_PATH}")
78
+
79
+
80
+ def save_config_to_file(config: dict[str, Any]) -> None:
81
+ """Save config to YAML file with deterministic ordering."""
82
+ tmp_path = CONFIG_PATH.with_suffix(CONFIG_PATH.suffix + ".tmp")
83
+ with tmp_path.open("w", encoding="utf-8") as f:
84
+ yaml.dump(
85
+ config,
86
+ f,
87
+ default_flow_style=False,
88
+ sort_keys=True,
89
+ allow_unicode=True,
90
+ )
91
+ tmp_path.replace(CONFIG_PATH)
92
+
93
+
94
+ # Global variable to store current config
95
+ config: dict[str, Any] = {}
96
+ config_lock = threading.Lock()
97
+
98
+
99
+ # =========================
100
+ # Supabase JWT verification
101
+ # =========================
102
+ SUPABASE_URL = os.getenv("SUPABASE_URL")
103
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY")
104
+ ACCOUNT_ID = os.getenv("ACCOUNT_ID") # optional: enforce instance ownership
105
+
106
+ _supabase_auth = None
107
+ if SUPABASE_URL and SUPABASE_ANON_KEY:
108
+ try:
109
+ from supabase import create_client
110
+
111
+ _supabase_auth = create_client(SUPABASE_URL, SUPABASE_ANON_KEY)
112
+ except Exception:
113
+ _supabase_auth = None
114
+
115
+
116
+ async def verify_user(authorization: str | None = Header(None)) -> dict:
117
+ """Validate Supabase JWT from Authorization header; enforce owner if ACCOUNT_ID set.
118
+
119
+ In standalone mode (no Supabase), returns a default user to allow access.
120
+ """
121
+ if _supabase_auth is None:
122
+ # Standalone mode - no auth configured, allow access
123
+ return {"user_id": "standalone", "email": None}
124
+
125
+ if not authorization or not authorization.startswith("Bearer "):
126
+ raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")
127
+
128
+ token = authorization.removeprefix("Bearer ").strip()
129
+ try:
130
+ user = _supabase_auth.auth.get_user(token)
131
+ except Exception as err:
132
+ raise HTTPException(status_code=401, detail="Invalid token") from err
133
+
134
+ if not user or not user.user:
135
+ raise HTTPException(status_code=401, detail="Invalid token")
136
+
137
+ if ACCOUNT_ID and user.user.id != ACCOUNT_ID:
138
+ raise HTTPException(status_code=403, detail="Forbidden")
139
+
140
+ return {"user_id": user.user.id, "email": user.user.email}
141
+
142
+
143
+ class TestModelRequest(BaseModel):
144
+ """Request model for testing AI model connections."""
145
+
146
+ modelId: str # noqa: N815
147
+
148
+
149
+ class ConfigFileHandler(FileSystemEventHandler):
150
+ """Watch for changes to config.yaml."""
151
+
152
+ def on_modified(self, event: FileSystemEvent) -> None:
153
+ """Handle file modification events."""
154
+ src_path = event.src_path
155
+ if isinstance(src_path, bytes):
156
+ src_path = src_path.decode("utf-8")
157
+ if src_path.endswith("config.yaml"):
158
+ print(f"Config file changed: {src_path}")
159
+ load_config_from_file()
160
+
161
+
162
+ def load_config_from_file() -> None:
163
+ """Load config from YAML file."""
164
+ global config
165
+ try:
166
+ with CONFIG_PATH.open() as f, config_lock:
167
+ config = yaml.safe_load(f)
168
+ print("Config loaded successfully")
169
+ except Exception as e:
170
+ print(f"Error loading config: {e}")
171
+
172
+
173
+ ensure_writable_config()
174
+
175
+ # Load initial config
176
+ load_config_from_file()
177
+
178
+ # Set up file watcher
179
+ observer = Observer()
180
+ observer.schedule(ConfigFileHandler(), path=str(CONFIG_PATH.parent), recursive=False)
181
+ observer.start()
182
+
183
+ # Include routers
184
+ app.include_router(credentials_router, dependencies=[Depends(verify_user)])
185
+ app.include_router(google_router, dependencies=[Depends(verify_user)])
186
+ app.include_router(homeassistant_router, dependencies=[Depends(verify_user)])
187
+ app.include_router(integrations_router, dependencies=[Depends(verify_user)])
188
+ app.include_router(matrix_router, dependencies=[Depends(verify_user)])
189
+ app.include_router(tools_router, dependencies=[Depends(verify_user)])
190
+
191
+
192
+ @app.get("/api/health")
193
+ async def health_check() -> dict[str, str]:
194
+ """Health check endpoint for testing."""
195
+ return {"status": "healthy"}
196
+
197
+
198
+ @app.on_event("startup")
199
+ async def startup_event() -> None:
200
+ """Initialize the application."""
201
+ print(f"Loading config from: {CONFIG_PATH}")
202
+ print(f"Config exists: {CONFIG_PATH.exists()}")
203
+
204
+ # Sync API keys from environment to CredentialsManager
205
+ print("Syncing API keys from environment to CredentialsManager...")
206
+ sync_env_to_credentials()
207
+
208
+
209
+ @app.on_event("shutdown")
210
+ async def shutdown_event() -> None:
211
+ """Clean up on shutdown."""
212
+ observer.stop()
213
+ observer.join()
214
+
215
+
216
+ @app.post("/api/config/load")
217
+ async def load_config(_user: Annotated[dict, Depends(verify_user)]) -> dict[str, Any]:
218
+ """Load configuration from file."""
219
+ with config_lock:
220
+ if not config:
221
+ raise HTTPException(status_code=500, detail="Failed to load configuration")
222
+ return config
223
+
224
+
225
+ @app.put("/api/config/save")
226
+ async def save_config(new_config: Config, _user: Annotated[dict, Depends(verify_user)]) -> dict[str, bool]:
227
+ """Save configuration to file."""
228
+ try:
229
+ config_dict = new_config.model_dump(exclude_none=True)
230
+ save_config_to_file(config_dict)
231
+
232
+ # Update current config
233
+ with config_lock:
234
+ config.update(config_dict)
235
+ except Exception as e:
236
+ raise HTTPException(status_code=500, detail=f"Failed to save configuration: {e!s}") from e
237
+ else:
238
+ return {"success": True}
239
+
240
+
241
+ @app.get("/api/config/agents")
242
+ async def get_agents(_user: Annotated[dict, Depends(verify_user)]) -> list[dict[str, Any]]:
243
+ """Get all agents."""
244
+ with config_lock:
245
+ agents = config.get("agents", {})
246
+ # Convert to list format with IDs
247
+ agent_list = []
248
+ for agent_id, agent_data in agents.items():
249
+ agent = {"id": agent_id, **agent_data}
250
+ agent_list.append(agent)
251
+ return agent_list
252
+
253
+
254
+ @app.put("/api/config/agents/{agent_id}")
255
+ async def update_agent(
256
+ agent_id: str,
257
+ agent_data: dict[str, Any],
258
+ _user: Annotated[dict, Depends(verify_user)],
259
+ ) -> dict[str, bool]:
260
+ """Update a specific agent."""
261
+ with config_lock:
262
+ if "agents" not in config:
263
+ config["agents"] = {}
264
+
265
+ # Remove ID from agent_data if present
266
+ agent_data_copy = agent_data.copy()
267
+ agent_data_copy.pop("id", None)
268
+
269
+ config["agents"][agent_id] = agent_data_copy
270
+
271
+ # Save to file
272
+ try:
273
+ save_config_to_file(config)
274
+ except Exception as e:
275
+ raise HTTPException(status_code=500, detail=f"Failed to save agent: {e!s}") from e
276
+ else:
277
+ return {"success": True}
278
+
279
+
280
+ @app.post("/api/config/agents")
281
+ async def create_agent(agent_data: dict[str, Any], _user: Annotated[dict, Depends(verify_user)]) -> dict[str, Any]:
282
+ """Create a new agent."""
283
+ agent_id = agent_data.get("display_name", "new_agent").lower().replace(" ", "_")
284
+
285
+ with config_lock:
286
+ if "agents" not in config:
287
+ config["agents"] = {}
288
+
289
+ # Check if agent already exists
290
+ if agent_id in config["agents"]:
291
+ # Generate unique ID
292
+ counter = 1
293
+ while f"{agent_id}_{counter}" in config["agents"]:
294
+ counter += 1
295
+ agent_id = f"{agent_id}_{counter}"
296
+
297
+ # Remove ID from agent_data if present
298
+ agent_data_copy = agent_data.copy()
299
+ agent_data_copy.pop("id", None)
300
+
301
+ config["agents"][agent_id] = agent_data_copy
302
+
303
+ # Save to file
304
+ try:
305
+ save_config_to_file(config)
306
+ except Exception as e:
307
+ raise HTTPException(status_code=500, detail=f"Failed to create agent: {e!s}") from e
308
+ else:
309
+ return {"id": agent_id, "success": True}
310
+
311
+
312
+ @app.delete("/api/config/agents/{agent_id}")
313
+ async def delete_agent(agent_id: str, _user: Annotated[dict, Depends(verify_user)]) -> dict[str, bool]:
314
+ """Delete an agent."""
315
+ with config_lock:
316
+ if "agents" not in config or agent_id not in config["agents"]:
317
+ raise HTTPException(status_code=404, detail="Agent not found")
318
+
319
+ del config["agents"][agent_id]
320
+
321
+ # Save to file
322
+ try:
323
+ save_config_to_file(config)
324
+ except Exception as e:
325
+ raise HTTPException(status_code=500, detail=f"Failed to delete agent: {e!s}") from e
326
+ else:
327
+ return {"success": True}
328
+
329
+
330
+ @app.get("/api/config/teams")
331
+ async def get_teams() -> list[dict[str, Any]]:
332
+ """Get all teams."""
333
+ with config_lock:
334
+ teams = config.get("teams", {})
335
+ # Convert to list format with IDs
336
+ team_list = []
337
+ for team_id, team_data in teams.items():
338
+ team = {"id": team_id, **team_data}
339
+ team_list.append(team)
340
+ return team_list
341
+
342
+
343
+ @app.put("/api/config/teams/{team_id}")
344
+ async def update_team(team_id: str, team_data: dict[str, Any]) -> dict[str, bool]:
345
+ """Update a specific team."""
346
+ with config_lock:
347
+ if "teams" not in config:
348
+ config["teams"] = {}
349
+
350
+ # Remove ID from team_data if present
351
+ team_data_copy = team_data.copy()
352
+ team_data_copy.pop("id", None)
353
+
354
+ config["teams"][team_id] = team_data_copy
355
+
356
+ # Save to file
357
+ try:
358
+ save_config_to_file(config)
359
+ except Exception as e:
360
+ raise HTTPException(status_code=500, detail=f"Failed to save team: {e!s}") from e
361
+ else:
362
+ return {"success": True}
363
+
364
+
365
+ @app.post("/api/config/teams")
366
+ async def create_team(team_data: dict[str, Any]) -> dict[str, Any]:
367
+ """Create a new team."""
368
+ team_id = team_data.get("display_name", "new_team").lower().replace(" ", "_")
369
+
370
+ with config_lock:
371
+ if "teams" not in config:
372
+ config["teams"] = {}
373
+
374
+ # Check if team already exists
375
+ if team_id in config["teams"]:
376
+ # Generate unique ID
377
+ counter = 1
378
+ while f"{team_id}_{counter}" in config["teams"]:
379
+ counter += 1
380
+ team_id = f"{team_id}_{counter}"
381
+
382
+ # Remove ID from team_data if present
383
+ team_data_copy = team_data.copy()
384
+ team_data_copy.pop("id", None)
385
+
386
+ config["teams"][team_id] = team_data_copy
387
+
388
+ # Save to file
389
+ try:
390
+ save_config_to_file(config)
391
+ except Exception as e:
392
+ raise HTTPException(status_code=500, detail=f"Failed to create team: {e!s}") from e
393
+ else:
394
+ return {"id": team_id, "success": True}
395
+
396
+
397
+ @app.delete("/api/config/teams/{team_id}")
398
+ async def delete_team(team_id: str) -> dict[str, bool]:
399
+ """Delete a team."""
400
+ with config_lock:
401
+ if "teams" not in config or team_id not in config["teams"]:
402
+ raise HTTPException(status_code=404, detail="Team not found")
403
+
404
+ del config["teams"][team_id]
405
+
406
+ # Save to file
407
+ try:
408
+ save_config_to_file(config)
409
+ except Exception as e:
410
+ raise HTTPException(status_code=500, detail=f"Failed to delete team: {e!s}") from e
411
+ else:
412
+ return {"success": True}
413
+
414
+
415
+ @app.get("/api/config/models")
416
+ async def get_models() -> dict[str, Any]:
417
+ """Get all model configurations."""
418
+ with config_lock:
419
+ models = config.get("models", {})
420
+ return dict(models) if models else {}
421
+
422
+
423
+ @app.put("/api/config/models/{model_id}")
424
+ async def update_model(model_id: str, model_data: dict[str, Any]) -> dict[str, bool]:
425
+ """Update a model configuration."""
426
+ with config_lock:
427
+ if "models" not in config:
428
+ config["models"] = {}
429
+
430
+ config["models"][model_id] = model_data
431
+
432
+ # Save to file
433
+ try:
434
+ save_config_to_file(config)
435
+ except Exception as e:
436
+ raise HTTPException(status_code=500, detail=f"Failed to save model: {e!s}") from e
437
+ else:
438
+ return {"success": True}
439
+
440
+
441
+ @app.get("/api/config/room-models")
442
+ async def get_room_models() -> dict[str, Any]:
443
+ """Get room-specific model overrides."""
444
+ with config_lock:
445
+ room_models = config.get("room_models", {})
446
+ return dict(room_models) if room_models else {}
447
+
448
+
449
+ @app.put("/api/config/room-models")
450
+ async def update_room_models(room_models: dict[str, str]) -> dict[str, bool]:
451
+ """Update room-specific model overrides."""
452
+ with config_lock:
453
+ config["room_models"] = room_models
454
+
455
+ # Save to file
456
+ try:
457
+ save_config_to_file(config)
458
+ except Exception as e:
459
+ raise HTTPException(status_code=500, detail=f"Failed to save room models: {e!s}") from e
460
+ else:
461
+ return {"success": True}
462
+
463
+
464
+ @app.post("/api/test/model")
465
+ async def test_model(request: TestModelRequest) -> dict[str, Any]:
466
+ """Test a model connection."""
467
+ # TODO: Implement actual model testing
468
+ # For now, just return success for demonstration
469
+ model_id = request.modelId
470
+ with config_lock:
471
+ if model_id in config.get("models", {}):
472
+ return {"success": True, "message": f"Model {model_id} is configured"}
473
+ return {"success": False, "message": f"Model {model_id} not found"}
474
+
475
+
476
+ @app.get("/api/rooms")
477
+ async def get_available_rooms() -> list[str]:
478
+ """Get list of available rooms."""
479
+ # Extract unique rooms from all agents
480
+ rooms = set()
481
+ with config_lock:
482
+ for agent_data in config.get("agents", {}).values():
483
+ agent_rooms = agent_data.get("rooms", [])
484
+ rooms.update(agent_rooms)
485
+
486
+ return sorted(rooms)
487
+
488
+
489
+ @app.post("/api/keys/encrypt")
490
+ async def encrypt_api_key(data: dict[str, str]) -> dict[str, str]:
491
+ """Encrypt an API key for storage."""
492
+ # TODO: Implement actual encryption
493
+ # For now, just return a placeholder
494
+ provider = data.get("provider", "")
495
+ key = data.get("key", "")
496
+
497
+ # In production, this would encrypt the key
498
+ encrypted = f"encrypted_{provider}_{len(key)}"
499
+
500
+ return {"encryptedKey": encrypted}
501
+
502
+
503
+ if __name__ == "__main__":
504
+ import uvicorn
505
+
506
+ uvicorn.run(app, host="0.0.0.0", port=8765) # noqa: S104