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.
- 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.0.dist-info/METADATA +425 -0
- mindroom-0.1.0.dist-info/RECORD +152 -0
- {mindroom-0.0.0.dist-info → mindroom-0.1.0.dist-info}/WHEEL +1 -2
- mindroom-0.1.0.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/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
|