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.
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.1.dist-info/METADATA +425 -0
  150. mindroom-0.1.1.dist-info/RECORD +152 -0
  151. {mindroom-0.0.0.dist-info → mindroom-0.1.1.dist-info}/WHEEL +1 -2
  152. mindroom-0.1.1.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
@@ -0,0 +1,219 @@
1
+ """API endpoints for Matrix operations."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from fastapi import APIRouter, HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from mindroom.constants import MATRIX_HOMESERVER
10
+ from mindroom.logging_config import get_logger
11
+ from mindroom.matrix.client import get_joined_rooms, get_room_name, leave_room
12
+ from mindroom.matrix.rooms import resolve_room_aliases
13
+ from mindroom.matrix.users import create_agent_user, login_agent_user
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ router = APIRouter(prefix="/api/matrix", tags=["matrix"])
18
+
19
+
20
+ class RoomLeaveRequest(BaseModel):
21
+ """Request to leave a room."""
22
+
23
+ agent_id: str
24
+ room_id: str
25
+
26
+
27
+ class RoomInfo(BaseModel):
28
+ """Information about a room."""
29
+
30
+ room_id: str
31
+ name: str | None = None
32
+
33
+
34
+ class AgentRoomsResponse(BaseModel):
35
+ """Response containing agent rooms information."""
36
+
37
+ agent_id: str
38
+ display_name: str
39
+ configured_rooms: list[str]
40
+ joined_rooms: list[str]
41
+ unconfigured_rooms: list[str]
42
+ unconfigured_room_details: list[RoomInfo] = []
43
+
44
+
45
+ class AllAgentsRoomsResponse(BaseModel):
46
+ """Response containing all agents' room information."""
47
+
48
+ agents: list[AgentRoomsResponse]
49
+
50
+
51
+ async def get_agent_matrix_rooms(agent_id: str, agent_data: dict[str, Any]) -> AgentRoomsResponse:
52
+ """Get Matrix rooms for a specific agent.
53
+
54
+ Args:
55
+ agent_id: The agent identifier
56
+ agent_data: The agent configuration data
57
+
58
+ Returns:
59
+ AgentRoomsResponse with room information
60
+
61
+ """
62
+ # Create or get the agent user
63
+ agent_user = await create_agent_user(
64
+ MATRIX_HOMESERVER,
65
+ agent_id,
66
+ agent_data.get("display_name", agent_id),
67
+ )
68
+
69
+ # Login and get the client
70
+ client = await login_agent_user(MATRIX_HOMESERVER, agent_user)
71
+
72
+ # Get all joined rooms from Matrix
73
+ joined_rooms = await get_joined_rooms(client) or []
74
+
75
+ # Get configured rooms from config (these are aliases like "lobby", "analysis")
76
+ configured_room_aliases = agent_data.get("rooms", [])
77
+
78
+ # Resolve room aliases to room IDs for comparison
79
+ configured_room_ids = resolve_room_aliases(configured_room_aliases)
80
+
81
+ # Calculate unconfigured rooms (joined but not in config)
82
+ unconfigured_rooms = [room for room in joined_rooms if room not in configured_room_ids]
83
+
84
+ # Get room names for unconfigured rooms
85
+ unconfigured_room_details = []
86
+ for room_id in unconfigured_rooms:
87
+ room_name = await get_room_name(client, room_id)
88
+ unconfigured_room_details.append(RoomInfo(room_id=room_id, name=room_name))
89
+
90
+ await client.close()
91
+
92
+ return AgentRoomsResponse(
93
+ agent_id=agent_id,
94
+ display_name=agent_data.get("display_name", agent_id),
95
+ configured_rooms=configured_room_ids,
96
+ joined_rooms=joined_rooms,
97
+ unconfigured_rooms=unconfigured_rooms,
98
+ unconfigured_room_details=unconfigured_room_details,
99
+ )
100
+
101
+
102
+ @router.get("/agents/rooms")
103
+ async def get_all_agents_rooms() -> AllAgentsRoomsResponse:
104
+ """Get room information for all agents.
105
+
106
+ Returns information about configured rooms, joined rooms,
107
+ and unconfigured rooms (joined but not in config) for each agent.
108
+ """
109
+ from .main import config, config_lock # noqa: PLC0415
110
+
111
+ agents_rooms = []
112
+
113
+ with config_lock:
114
+ agents = config.get("agents", {})
115
+
116
+ # Gather room information for all agents concurrently
117
+ tasks = [get_agent_matrix_rooms(agent_id, agent_data) for agent_id, agent_data in agents.items()]
118
+ agents_rooms = await asyncio.gather(*tasks)
119
+
120
+ return AllAgentsRoomsResponse(agents=agents_rooms)
121
+
122
+
123
+ @router.get("/agents/{agent_id}/rooms")
124
+ async def get_agent_rooms(agent_id: str) -> AgentRoomsResponse:
125
+ """Get room information for a specific agent.
126
+
127
+ Args:
128
+ agent_id: The agent identifier
129
+
130
+ Returns:
131
+ Room information for the agent
132
+
133
+ Raises:
134
+ HTTPException: If agent not found or error occurs
135
+
136
+ """
137
+ from .main import config, config_lock # noqa: PLC0415
138
+
139
+ with config_lock:
140
+ agents = config.get("agents", {})
141
+ if agent_id not in agents:
142
+ raise HTTPException(status_code=404, detail=f"Agent {agent_id} not found")
143
+ agent_data = agents[agent_id]
144
+
145
+ return await get_agent_matrix_rooms(agent_id, agent_data)
146
+
147
+
148
+ @router.post("/rooms/leave")
149
+ async def leave_room_endpoint(request: RoomLeaveRequest) -> dict[str, bool]:
150
+ """Make an agent leave a specific room.
151
+
152
+ Args:
153
+ request: Contains agent_id and room_id
154
+
155
+ Returns:
156
+ Success status
157
+
158
+ Raises:
159
+ HTTPException: If agent not found or leave operation fails
160
+
161
+ """
162
+ from .main import config, config_lock # noqa: PLC0415
163
+
164
+ with config_lock:
165
+ agents = config.get("agents", {})
166
+ if request.agent_id not in agents:
167
+ raise HTTPException(status_code=404, detail=f"Agent {request.agent_id} not found")
168
+
169
+ # Get agent configuration
170
+ agent_data = agents[request.agent_id]
171
+
172
+ # Create or get the agent user
173
+ agent_user = await create_agent_user(
174
+ MATRIX_HOMESERVER,
175
+ request.agent_id,
176
+ agent_data.get("display_name", request.agent_id),
177
+ )
178
+
179
+ # Login and get the client
180
+ client = await login_agent_user(MATRIX_HOMESERVER, agent_user)
181
+
182
+ # Leave the room
183
+ success = await leave_room(client, request.room_id)
184
+
185
+ # Close the client connection
186
+ await client.close()
187
+
188
+ if not success:
189
+ raise HTTPException(status_code=500, detail=f"Failed to leave room {request.room_id}")
190
+ return {"success": True}
191
+
192
+
193
+ @router.post("/rooms/leave-bulk")
194
+ async def leave_rooms_bulk(requests: list[RoomLeaveRequest]) -> dict[str, Any]:
195
+ """Make multiple agents leave multiple rooms.
196
+
197
+ Args:
198
+ requests: List of leave requests
199
+
200
+ Returns:
201
+ Results for each request
202
+
203
+ """
204
+ results = []
205
+ for request in requests:
206
+ try:
207
+ await leave_room_endpoint(request)
208
+ results.append({"agent_id": request.agent_id, "room_id": request.room_id, "success": True})
209
+ except HTTPException as e:
210
+ results.append(
211
+ {
212
+ "agent_id": request.agent_id,
213
+ "room_id": request.room_id,
214
+ "success": False,
215
+ "error": e.detail,
216
+ },
217
+ )
218
+
219
+ return {"results": results, "success": all(r["success"] for r in results)}
mindroom/api/tools.py ADDED
@@ -0,0 +1,94 @@
1
+ """API endpoints for tools information."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, HTTPException
8
+ from pydantic import BaseModel
9
+
10
+ import mindroom
11
+ from mindroom.credentials import CredentialsManager, get_credentials_manager
12
+
13
+ from .google_tools_helper import check_google_tool_configured
14
+
15
+ router = APIRouter(prefix="/api/tools", tags=["tools"])
16
+
17
+
18
+ class ToolsResponse(BaseModel):
19
+ """Response containing all registered tools."""
20
+
21
+ tools: list[dict]
22
+
23
+
24
+ def _check_homeassistant_configured(tool_name: str, manager: CredentialsManager) -> bool:
25
+ """Check if HomeAssistant is configured."""
26
+ if tool_name == "homeassistant":
27
+ ha_creds = manager.load_credentials("homeassistant")
28
+ if not ha_creds:
29
+ return False
30
+ # Check for the fields that HomeAssistantTools actually uses
31
+ has_url = "instance_url" in ha_creds
32
+ has_token = "access_token" in ha_creds or "long_lived_token" in ha_creds
33
+ return has_url and has_token
34
+ return False
35
+
36
+
37
+ def _check_standard_tool_configured(tool: dict[str, Any], manager: CredentialsManager) -> bool:
38
+ """Check if a standard tool with config_fields is configured."""
39
+ if not tool.get("config_fields"):
40
+ return False
41
+
42
+ credentials = manager.load_credentials(tool["name"])
43
+ if not credentials:
44
+ return False
45
+
46
+ # Check if all required fields are present
47
+ required_fields = [field["name"] for field in tool.get("config_fields", []) if field.get("required", True)]
48
+ return all(field in credentials for field in required_fields)
49
+
50
+
51
+ @router.get("")
52
+ async def get_registered_tools() -> ToolsResponse:
53
+ """Get all registered tools from mindroom.
54
+
55
+ This reads from a pre-generated JSON file that is created by the test suite.
56
+ The JSON file is generated by tests/test_tools_metadata.py and committed to the repo.
57
+ It also checks if credentials exist for tools that require them.
58
+ """
59
+ # Path to the generated JSON file - use package location for reliability
60
+ package_dir = Path(mindroom.__file__).parent
61
+ json_path = package_dir / "tools_metadata.json"
62
+
63
+ if not json_path.exists():
64
+ raise HTTPException(
65
+ status_code=500,
66
+ detail="tools_metadata.json not found. Run 'pytest tests/test_tools_metadata.py' to generate it.",
67
+ )
68
+
69
+ with json_path.open() as f:
70
+ data = json.load(f)
71
+ tools = data["tools"]
72
+
73
+ # Get credentials manager to check if tools are configured
74
+ manager = get_credentials_manager()
75
+
76
+ # Update status for tools that require configuration
77
+ for tool in tools:
78
+ tool_name = tool["name"]
79
+ if tool.get("status") == "requires_config":
80
+ # Check if tool has delegated auth
81
+ auth_provider = tool.get("auth_provider")
82
+ if auth_provider:
83
+ # Check if the auth provider is configured
84
+ provider_creds = manager.load_credentials(auth_provider)
85
+ if provider_creds and (
86
+ (auth_provider == "google" and check_google_tool_configured(tool_name, provider_creds))
87
+ or auth_provider != "google"
88
+ ):
89
+ tool["status"] = "available"
90
+ # Check other configured tools
91
+ elif _check_homeassistant_configured(tool_name, manager) or _check_standard_tool_configured(tool, manager):
92
+ tool["status"] = "available"
93
+
94
+ return ToolsResponse(tools=tools)
@@ -0,0 +1,87 @@
1
+ """Background task management for non-blocking operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from .logging_config import get_logger
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Callable, Coroutine
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ # Global set to track background tasks and prevent them from being garbage collected
16
+ _background_tasks: set[asyncio.Task[Any]] = set()
17
+
18
+
19
+ def create_background_task(
20
+ coro: Coroutine[Any, Any, Any],
21
+ name: str | None = None,
22
+ error_handler: Callable[[Exception], None] | None = None,
23
+ ) -> asyncio.Task[Any]:
24
+ """Create a background task that won't block the main execution.
25
+
26
+ Args:
27
+ coro: The coroutine to run in the background
28
+ name: Optional name for the task (for logging)
29
+ error_handler: Optional error handler function
30
+
31
+ Returns:
32
+ The created task
33
+
34
+ """
35
+ task: asyncio.Task[Any] = asyncio.create_task(coro)
36
+ if name:
37
+ task.set_name(name)
38
+
39
+ # Add to global set to prevent garbage collection
40
+ _background_tasks.add(task)
41
+
42
+ # Add completion callback to remove from set and handle errors
43
+ def _task_done_callback(task: asyncio.Task[Any]) -> None:
44
+ _background_tasks.discard(task)
45
+ try:
46
+ # This will raise if the task had an exception
47
+ task.result()
48
+ except asyncio.CancelledError:
49
+ # Task was cancelled, this is fine
50
+ pass
51
+ except Exception as e:
52
+ task_name = task.get_name() if hasattr(task, "get_name") else "unknown"
53
+ logger.exception("Background task failed", task_name=task_name, error=str(e))
54
+ if error_handler:
55
+ try:
56
+ error_handler(e)
57
+ except Exception as handler_error:
58
+ logger.exception("Error handler for task failed", task_name=task_name, error=str(handler_error))
59
+
60
+ task.add_done_callback(_task_done_callback)
61
+ return task
62
+
63
+
64
+ async def wait_for_background_tasks(timeout: float | None = None) -> None: # noqa: ASYNC109
65
+ """Wait for all background tasks to complete.
66
+
67
+ Args:
68
+ timeout: Optional timeout in seconds
69
+
70
+ """
71
+ if not _background_tasks:
72
+ return
73
+
74
+ try:
75
+ await asyncio.wait_for(asyncio.gather(*_background_tasks, return_exceptions=True), timeout=timeout)
76
+ except TimeoutError:
77
+ logger.warning(f"Background tasks did not complete within {timeout} seconds")
78
+ # Cancel remaining tasks
79
+ for task in _background_tasks:
80
+ task.cancel()
81
+ # Wait for cancellation to complete
82
+ await asyncio.gather(*_background_tasks, return_exceptions=True)
83
+
84
+
85
+ def get_background_task_count() -> int:
86
+ """Get the number of currently running background tasks."""
87
+ return len(_background_tasks)