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