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,92 @@
1
+ """Custom Gmail Tools wrapper for MindRoom.
2
+
3
+ This module provides a wrapper around Agno's GmailTools that properly handles
4
+ credentials stored in MindRoom's unified credentials location.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from agno.tools.gmail import GmailTools as AgnoGmailTools
10
+ from google.auth.transport.requests import Request
11
+ from google.oauth2.credentials import Credentials
12
+ from loguru import logger
13
+
14
+ from mindroom.credentials import get_credentials_manager
15
+
16
+
17
+ class GmailTools(AgnoGmailTools):
18
+ """Gmail tools wrapper that uses MindRoom's credential management."""
19
+
20
+ def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
21
+ """Initialize Gmail tools with MindRoom credentials.
22
+
23
+ This wrapper automatically loads credentials from MindRoom's
24
+ unified credential storage and passes them to the Agno GmailTools.
25
+ """
26
+ # Load credentials using the credentials manager
27
+ creds_manager = get_credentials_manager()
28
+ token_data = creds_manager.load_credentials("google")
29
+ creds = None
30
+
31
+ if token_data:
32
+ try:
33
+ # Create Google Credentials object from stored data
34
+ creds = Credentials(
35
+ token=token_data.get("token"),
36
+ refresh_token=token_data.get("refresh_token"),
37
+ token_uri=token_data.get("token_uri"),
38
+ client_id=token_data.get("client_id"),
39
+ client_secret=token_data.get("client_secret"),
40
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
41
+ )
42
+ logger.info("Loaded Gmail credentials from MindRoom storage")
43
+ except Exception as e:
44
+ logger.error(f"Failed to load Gmail credentials: {e}")
45
+ creds = None
46
+ else:
47
+ logger.warning("Gmail credentials not found in MindRoom storage")
48
+
49
+ # Pass credentials to parent class
50
+ super().__init__(creds=creds, **kwargs)
51
+
52
+ # Store original auth method for fallback
53
+ self._original_auth = super()._auth
54
+
55
+ def _auth(self) -> None:
56
+ """Custom auth method that uses MindRoom's credential storage."""
57
+ # If we already have valid credentials, don't re-authenticate
58
+ if self.creds and self.creds.valid:
59
+ return
60
+
61
+ # Reload credentials from MindRoom's storage in case they've been updated
62
+ creds_manager = get_credentials_manager()
63
+ token_data = creds_manager.load_credentials("google")
64
+
65
+ if token_data:
66
+ try:
67
+ self.creds = Credentials(
68
+ token=token_data.get("token"),
69
+ refresh_token=token_data.get("refresh_token"),
70
+ token_uri=token_data.get("token_uri"),
71
+ client_id=token_data.get("client_id"),
72
+ client_secret=token_data.get("client_secret"),
73
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
74
+ )
75
+
76
+ # Refresh if expired
77
+ if self.creds.expired and self.creds.refresh_token:
78
+ self.creds.refresh(Request())
79
+
80
+ # Save the refreshed credentials back
81
+ token_data["token"] = self.creds.token
82
+ creds_manager.save_credentials("google", token_data)
83
+
84
+ logger.info("Gmail authentication successful")
85
+ except Exception as e:
86
+ logger.error(f"Failed to authenticate with Gmail: {e}")
87
+ raise
88
+ else:
89
+ # If no credentials found, fall back to original auth method
90
+ # This will prompt for OAuth flow
91
+ logger.warning("No stored credentials found, initiating OAuth flow")
92
+ self._original_auth()
@@ -0,0 +1,92 @@
1
+ """Custom Google Calendar Tools wrapper for MindRoom.
2
+
3
+ This module provides a wrapper around Agno's GoogleCalendarTools that properly handles
4
+ credentials stored in MindRoom's unified credentials location.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from agno.tools.googlecalendar import GoogleCalendarTools as AgnoGoogleCalendarTools
10
+ from google.auth.transport.requests import Request
11
+ from google.oauth2.credentials import Credentials
12
+ from loguru import logger
13
+
14
+ from mindroom.credentials import get_credentials_manager
15
+
16
+
17
+ class GoogleCalendarTools(AgnoGoogleCalendarTools):
18
+ """Google Calendar tools wrapper that uses MindRoom's credential management."""
19
+
20
+ def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
21
+ """Initialize Google Calendar tools with MindRoom credentials.
22
+
23
+ This wrapper automatically loads credentials from MindRoom's
24
+ unified credential storage and passes them to the Agno GoogleCalendarTools.
25
+ """
26
+ # Load credentials using the credentials manager
27
+ creds_manager = get_credentials_manager()
28
+ token_data = creds_manager.load_credentials("google")
29
+ creds = None
30
+
31
+ if token_data:
32
+ try:
33
+ # Create Google Credentials object from stored data
34
+ creds = Credentials(
35
+ token=token_data.get("token"),
36
+ refresh_token=token_data.get("refresh_token"),
37
+ token_uri=token_data.get("token_uri"),
38
+ client_id=token_data.get("client_id"),
39
+ client_secret=token_data.get("client_secret"),
40
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
41
+ )
42
+ logger.info("Loaded Google Calendar credentials from MindRoom storage")
43
+ except Exception as e:
44
+ logger.error(f"Failed to load Google Calendar credentials: {e}")
45
+ creds = None
46
+ else:
47
+ logger.warning("Google Calendar credentials not found in MindRoom storage")
48
+
49
+ # Pass credentials to parent class
50
+ super().__init__(creds=creds, **kwargs)
51
+
52
+ # Store original auth method for fallback
53
+ self._original_auth = super()._auth
54
+
55
+ def _auth(self) -> None:
56
+ """Custom auth method that uses MindRoom's credential storage."""
57
+ # If we already have valid credentials, don't re-authenticate
58
+ if self.creds and self.creds.valid:
59
+ return
60
+
61
+ # Reload credentials from MindRoom's storage in case they've been updated
62
+ creds_manager = get_credentials_manager()
63
+ token_data = creds_manager.load_credentials("google")
64
+
65
+ if token_data:
66
+ try:
67
+ self.creds = Credentials(
68
+ token=token_data.get("token"),
69
+ refresh_token=token_data.get("refresh_token"),
70
+ token_uri=token_data.get("token_uri"),
71
+ client_id=token_data.get("client_id"),
72
+ client_secret=token_data.get("client_secret"),
73
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
74
+ )
75
+
76
+ # Refresh if expired
77
+ if self.creds.expired and self.creds.refresh_token:
78
+ self.creds.refresh(Request())
79
+
80
+ # Save the refreshed credentials back
81
+ token_data["token"] = self.creds.token
82
+ creds_manager.save_credentials("google", token_data)
83
+
84
+ logger.info("Google Calendar authentication successful")
85
+ except Exception as e:
86
+ logger.error(f"Failed to authenticate with Google Calendar: {e}")
87
+ raise
88
+ else:
89
+ # If no credentials found, fall back to original auth method
90
+ # This will prompt for OAuth flow
91
+ logger.warning("No stored credentials found, initiating OAuth flow")
92
+ self._original_auth()
@@ -0,0 +1,92 @@
1
+ """Custom Google Sheets Tools wrapper for MindRoom.
2
+
3
+ This module provides a wrapper around Agno's GoogleSheetsTools that properly handles
4
+ credentials stored in MindRoom's unified credentials location.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from agno.tools.googlesheets import GoogleSheetsTools as AgnoGoogleSheetsTools
10
+ from google.auth.transport.requests import Request
11
+ from google.oauth2.credentials import Credentials
12
+ from loguru import logger
13
+
14
+ from mindroom.credentials import get_credentials_manager
15
+
16
+
17
+ class GoogleSheetsTools(AgnoGoogleSheetsTools):
18
+ """Google Sheets tools wrapper that uses MindRoom's credential management."""
19
+
20
+ def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
21
+ """Initialize Google Sheets tools with MindRoom credentials.
22
+
23
+ This wrapper automatically loads credentials from MindRoom's
24
+ unified credential storage and passes them to the Agno GoogleSheetsTools.
25
+ """
26
+ # Load credentials using the credentials manager
27
+ creds_manager = get_credentials_manager()
28
+ token_data = creds_manager.load_credentials("google")
29
+ creds = None
30
+
31
+ if token_data:
32
+ try:
33
+ # Create Google Credentials object from stored data
34
+ creds = Credentials(
35
+ token=token_data.get("token"),
36
+ refresh_token=token_data.get("refresh_token"),
37
+ token_uri=token_data.get("token_uri"),
38
+ client_id=token_data.get("client_id"),
39
+ client_secret=token_data.get("client_secret"),
40
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
41
+ )
42
+ logger.info("Loaded Google Sheets credentials from MindRoom storage")
43
+ except Exception as e:
44
+ logger.error(f"Failed to load Google Sheets credentials: {e}")
45
+ creds = None
46
+ else:
47
+ logger.warning("Google Sheets credentials not found in MindRoom storage")
48
+
49
+ # Pass credentials to parent class
50
+ super().__init__(creds=creds, **kwargs)
51
+
52
+ # Store original auth method for fallback
53
+ self._original_auth = super()._auth
54
+
55
+ def _auth(self) -> None:
56
+ """Custom auth method that uses MindRoom's credential storage."""
57
+ # If we already have valid credentials, don't re-authenticate
58
+ if self.creds and self.creds.valid:
59
+ return
60
+
61
+ # Reload credentials from MindRoom's storage in case they've been updated
62
+ creds_manager = get_credentials_manager()
63
+ token_data = creds_manager.load_credentials("google")
64
+
65
+ if token_data:
66
+ try:
67
+ self.creds = Credentials(
68
+ token=token_data.get("token"),
69
+ refresh_token=token_data.get("refresh_token"),
70
+ token_uri=token_data.get("token_uri"),
71
+ client_id=token_data.get("client_id"),
72
+ client_secret=token_data.get("client_secret"),
73
+ scopes=token_data.get("scopes", self.DEFAULT_SCOPES),
74
+ )
75
+
76
+ # Refresh if expired
77
+ if self.creds.expired and self.creds.refresh_token:
78
+ self.creds.refresh(Request())
79
+
80
+ # Save the refreshed credentials back
81
+ token_data["token"] = self.creds.token
82
+ creds_manager.save_credentials("google", token_data)
83
+
84
+ logger.info("Google Sheets authentication successful")
85
+ except Exception as e:
86
+ logger.error(f"Failed to authenticate with Google Sheets: {e}")
87
+ raise
88
+ else:
89
+ # If no credentials found, fall back to original auth method
90
+ # This will prompt for OAuth flow
91
+ logger.warning("No stored credentials found, initiating OAuth flow")
92
+ self._original_auth()
@@ -0,0 +1,341 @@
1
+ """Home Assistant tools for MindRoom agents.
2
+
3
+ This module provides tools for interacting with Home Assistant,
4
+ allowing agents to control devices, query states, and execute automations.
5
+ """
6
+
7
+ import json
8
+ from typing import Any
9
+ from urllib.parse import urljoin
10
+
11
+ import httpx
12
+ from agno.tools import Toolkit
13
+
14
+ from mindroom.credentials import get_credentials_manager
15
+
16
+
17
+ class HomeAssistantTools(Toolkit):
18
+ """Tools for interacting with Home Assistant."""
19
+
20
+ def __init__(self) -> None:
21
+ """Initialize Home Assistant tools."""
22
+ # Use the credentials manager
23
+ self._creds_manager = get_credentials_manager()
24
+ self._config: dict[str, Any] | None = None
25
+
26
+ # Initialize the toolkit with all available methods
27
+ super().__init__(
28
+ name="homeassistant",
29
+ tools=[
30
+ self.get_entity_state,
31
+ self.list_entities,
32
+ self.turn_on,
33
+ self.turn_off,
34
+ self.toggle,
35
+ self.set_brightness,
36
+ self.set_color,
37
+ self.set_temperature,
38
+ self.activate_scene,
39
+ self.trigger_automation,
40
+ self.call_service,
41
+ ],
42
+ )
43
+
44
+ def _load_config(self) -> dict[str, Any] | None:
45
+ """Load Home Assistant configuration from unified location."""
46
+ if self._config:
47
+ return self._config
48
+
49
+ # Load from credentials manager
50
+ self._config = self._creds_manager.load_credentials("homeassistant")
51
+ return self._config
52
+
53
+ async def _api_request( # noqa: PLR0911
54
+ self,
55
+ method: str,
56
+ endpoint: str,
57
+ json_data: dict[str, Any] | None = None,
58
+ ) -> dict[str, Any]:
59
+ """Make an API request to Home Assistant."""
60
+ config = self._load_config()
61
+ if not config:
62
+ return {"error": "Home Assistant is not configured. Please connect through the widget."}
63
+
64
+ instance_url = config.get("instance_url")
65
+ token = config.get("access_token") or config.get("long_lived_token")
66
+
67
+ if not instance_url or not token:
68
+ return {"error": "Missing Home Assistant credentials"}
69
+
70
+ try:
71
+ async with httpx.AsyncClient() as client:
72
+ response = await client.request(
73
+ method=method,
74
+ url=urljoin(instance_url, endpoint),
75
+ headers={"Authorization": f"Bearer {token}"},
76
+ json=json_data,
77
+ timeout=10.0,
78
+ )
79
+
80
+ if response.status_code == 401:
81
+ return {"error": "Invalid authentication token. Please reconnect Home Assistant."}
82
+ if response.status_code not in (200, 201):
83
+ return {"error": f"API error: {response.text}"}
84
+
85
+ return response.json() if response.text else {"success": True}
86
+
87
+ except (httpx.ConnectTimeout, httpx.ReadTimeout, httpx.WriteTimeout, httpx.PoolTimeout):
88
+ return {"error": "Connection timeout - check if Home Assistant is accessible"}
89
+ except httpx.RequestError as e:
90
+ return {"error": f"Connection error: {e!s}"}
91
+ except Exception as e:
92
+ return {"error": f"Unexpected error: {e!s}"}
93
+
94
+ async def get_entity_state(self, entity_id: str) -> str:
95
+ """Get the current state of a Home Assistant entity.
96
+
97
+ Args:
98
+ entity_id: The entity ID (e.g., 'light.living_room', 'switch.bedroom_fan')
99
+
100
+ Returns:
101
+ JSON string with entity state information
102
+
103
+ """
104
+ result = await self._api_request("GET", f"/api/states/{entity_id}")
105
+
106
+ if "error" in result:
107
+ return json.dumps(result)
108
+
109
+ return json.dumps(
110
+ {
111
+ "entity_id": result.get("entity_id"),
112
+ "state": result.get("state"),
113
+ "attributes": result.get("attributes", {}),
114
+ "last_changed": result.get("last_changed"),
115
+ },
116
+ )
117
+
118
+ async def list_entities(self, domain: str = "") -> str:
119
+ """List all entities in Home Assistant, optionally filtered by domain.
120
+
121
+ Args:
122
+ domain: Optional domain to filter by (e.g., 'light', 'switch', 'sensor')
123
+
124
+ Returns:
125
+ JSON string with list of entities
126
+
127
+ """
128
+ result = await self._api_request("GET", "/api/states")
129
+
130
+ if isinstance(result, dict) and "error" in result:
131
+ return json.dumps(result)
132
+
133
+ entities = result
134
+
135
+ # Filter by domain if specified
136
+ if domain and isinstance(entities, list):
137
+ entities = [e for e in entities if e["entity_id"].startswith(f"{domain}.")]
138
+
139
+ # Simplify the response
140
+ entity_list: list[Any] = entities[:50] if isinstance(entities, list) else []
141
+ simplified: list[dict[str, Any]] = [
142
+ {
143
+ "entity_id": e["entity_id"],
144
+ "state": e["state"],
145
+ "friendly_name": e.get("attributes", {}).get("friendly_name", e["entity_id"]),
146
+ }
147
+ for e in entity_list # Limit to 50 entities to avoid huge responses
148
+ ]
149
+
150
+ return json.dumps(simplified)
151
+
152
+ async def turn_on(self, entity_id: str) -> str:
153
+ """Turn on a device (light, switch, etc.).
154
+
155
+ Args:
156
+ entity_id: The entity ID to turn on (e.g., 'light.living_room')
157
+
158
+ Returns:
159
+ JSON string with result
160
+
161
+ """
162
+ domain = entity_id.split(".")[0]
163
+ result = await self._api_request(
164
+ "POST",
165
+ f"/api/services/{domain}/turn_on",
166
+ {"entity_id": entity_id},
167
+ )
168
+ return json.dumps(result)
169
+
170
+ async def turn_off(self, entity_id: str) -> str:
171
+ """Turn off a device (light, switch, etc.).
172
+
173
+ Args:
174
+ entity_id: The entity ID to turn off (e.g., 'light.living_room')
175
+
176
+ Returns:
177
+ JSON string with result
178
+
179
+ """
180
+ domain = entity_id.split(".")[0]
181
+ result = await self._api_request(
182
+ "POST",
183
+ f"/api/services/{domain}/turn_off",
184
+ {"entity_id": entity_id},
185
+ )
186
+ return json.dumps(result)
187
+
188
+ async def toggle(self, entity_id: str) -> str:
189
+ """Toggle a device (if on, turn off; if off, turn on).
190
+
191
+ Args:
192
+ entity_id: The entity ID to toggle (e.g., 'switch.bedroom_fan')
193
+
194
+ Returns:
195
+ JSON string with result
196
+
197
+ """
198
+ domain = entity_id.split(".")[0]
199
+ result = await self._api_request(
200
+ "POST",
201
+ f"/api/services/{domain}/toggle",
202
+ {"entity_id": entity_id},
203
+ )
204
+ return json.dumps(result)
205
+
206
+ async def set_brightness(self, entity_id: str, brightness: int) -> str:
207
+ """Set the brightness of a light.
208
+
209
+ Args:
210
+ entity_id: The light entity ID (e.g., 'light.living_room')
211
+ brightness: Brightness level (0-255, where 255 is 100%)
212
+
213
+ Returns:
214
+ JSON string with result
215
+
216
+ """
217
+ if not 0 <= brightness <= 255:
218
+ return json.dumps({"error": "Brightness must be between 0 and 255"})
219
+
220
+ result = await self._api_request(
221
+ "POST",
222
+ "/api/services/light/turn_on",
223
+ {
224
+ "entity_id": entity_id,
225
+ "brightness": brightness,
226
+ },
227
+ )
228
+ return json.dumps(result)
229
+
230
+ async def set_color(self, entity_id: str, red: int, green: int, blue: int) -> str:
231
+ """Set the color of a light using RGB values.
232
+
233
+ Args:
234
+ entity_id: The light entity ID (e.g., 'light.living_room')
235
+ red: Red value (0-255)
236
+ green: Green value (0-255)
237
+ blue: Blue value (0-255)
238
+
239
+ Returns:
240
+ JSON string with result
241
+
242
+ """
243
+ if not all(0 <= v <= 255 for v in [red, green, blue]):
244
+ return json.dumps({"error": "RGB values must be between 0 and 255"})
245
+
246
+ result = await self._api_request(
247
+ "POST",
248
+ "/api/services/light/turn_on",
249
+ {
250
+ "entity_id": entity_id,
251
+ "rgb_color": [red, green, blue],
252
+ },
253
+ )
254
+ return json.dumps(result)
255
+
256
+ async def set_temperature(self, entity_id: str, temperature: float) -> str:
257
+ """Set the temperature of a climate device.
258
+
259
+ Args:
260
+ entity_id: The climate entity ID (e.g., 'climate.thermostat')
261
+ temperature: Target temperature in the unit configured in Home Assistant
262
+
263
+ Returns:
264
+ JSON string with result
265
+
266
+ """
267
+ result = await self._api_request(
268
+ "POST",
269
+ "/api/services/climate/set_temperature",
270
+ {
271
+ "entity_id": entity_id,
272
+ "temperature": temperature,
273
+ },
274
+ )
275
+ return json.dumps(result)
276
+
277
+ async def activate_scene(self, scene_id: str) -> str:
278
+ """Activate a Home Assistant scene.
279
+
280
+ Args:
281
+ scene_id: The scene entity ID (e.g., 'scene.movie_time')
282
+
283
+ Returns:
284
+ JSON string with result
285
+
286
+ """
287
+ result = await self._api_request(
288
+ "POST",
289
+ "/api/services/scene/turn_on",
290
+ {"entity_id": scene_id},
291
+ )
292
+ return json.dumps(result)
293
+
294
+ async def trigger_automation(self, automation_id: str) -> str:
295
+ """Trigger a Home Assistant automation.
296
+
297
+ Args:
298
+ automation_id: The automation entity ID (e.g., 'automation.morning_routine')
299
+
300
+ Returns:
301
+ JSON string with result
302
+
303
+ """
304
+ result = await self._api_request(
305
+ "POST",
306
+ "/api/services/automation/trigger",
307
+ {"entity_id": automation_id},
308
+ )
309
+ return json.dumps(result)
310
+
311
+ async def call_service(self, domain: str, service: str, entity_id: str = "", data: str = "") -> str:
312
+ """Call a generic Home Assistant service.
313
+
314
+ Args:
315
+ domain: The service domain (e.g., 'light', 'switch', 'notify')
316
+ service: The service name (e.g., 'turn_on', 'toggle', 'send_message')
317
+ entity_id: The entity ID(s) to apply the service to (optional)
318
+ data: Additional service data as JSON string (optional)
319
+
320
+ Returns:
321
+ JSON string with result
322
+
323
+ """
324
+ service_data = {}
325
+
326
+ if entity_id:
327
+ service_data["entity_id"] = entity_id
328
+
329
+ if data:
330
+ try:
331
+ additional_data = json.loads(data)
332
+ service_data.update(additional_data)
333
+ except json.JSONDecodeError:
334
+ return json.dumps({"error": "Invalid JSON in data parameter"})
335
+
336
+ result = await self._api_request(
337
+ "POST",
338
+ f"/api/services/{domain}/{service}",
339
+ service_data,
340
+ )
341
+ return json.dumps(result)
@@ -0,0 +1,35 @@
1
+ """Simple error handling for MindRoom agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .logging_config import get_logger
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ def get_user_friendly_error_message(error: Exception, agent_name: str | None = None) -> str:
11
+ """Return a user-friendly error message.
12
+
13
+ Args:
14
+ error: The exception that occurred
15
+ agent_name: Optional name of the agent that encountered the error
16
+
17
+ Returns:
18
+ A user-friendly error message
19
+
20
+ """
21
+ error_str = str(error).lower()
22
+ agent_prefix = f"[{agent_name}] " if agent_name else ""
23
+
24
+ # Log the full error for debugging
25
+ logger.error(f"Error in {agent_name or 'agent'}: {error!r}")
26
+
27
+ # Only distinguish the most important error types
28
+ if any(x in error_str for x in ["api", "401", "auth", "key", "unauthorized"]):
29
+ return f"{agent_prefix}❌ Authentication failed. Please check your API key configuration."
30
+ if any(x in error_str for x in ["rate", "429", "quota"]):
31
+ return f"{agent_prefix}⏱️ Rate limited. Please wait a moment and try again."
32
+ if "timeout" in error_str:
33
+ return f"{agent_prefix}⏰ Request timed out. Please try again."
34
+ # Generic error with the actual error message for transparency
35
+ return f"{agent_prefix}⚠️ Error: {error!s}"