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
|
@@ -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}"
|