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.
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.0.dist-info/METADATA +425 -0
  150. mindroom-0.1.0.dist-info/RECORD +152 -0
  151. {mindroom-0.0.0.dist-info โ†’ mindroom-0.1.0.dist-info}/WHEEL +1 -2
  152. mindroom-0.1.0.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,49 @@
1
+ """Simple file watcher utility without external dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import structlog
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Awaitable, Callable
13
+
14
+ logger = structlog.get_logger(__name__)
15
+
16
+
17
+ async def watch_file(
18
+ file_path: Path | str,
19
+ callback: Callable[[], Awaitable[None]],
20
+ stop_event: asyncio.Event | None = None,
21
+ ) -> None:
22
+ """Watch a file for changes and call callback when modified.
23
+
24
+ Args:
25
+ file_path: Path to the file to watch
26
+ callback: Async function to call when file changes
27
+ stop_event: Optional event to signal when to stop watching
28
+
29
+ """
30
+ file_path = Path(file_path)
31
+ last_mtime = file_path.stat().st_mtime if file_path.exists() else 0
32
+
33
+ while stop_event is None or not stop_event.is_set():
34
+ await asyncio.sleep(1.0) # Check every second
35
+
36
+ try:
37
+ if file_path.exists():
38
+ current_mtime = file_path.stat().st_mtime
39
+ if current_mtime != last_mtime:
40
+ last_mtime = current_mtime
41
+ await callback()
42
+ except (OSError, PermissionError):
43
+ # File might have been deleted or become unreadable
44
+ # Reset mtime so we detect when it comes back
45
+ last_mtime = 0
46
+ except Exception:
47
+ # Don't let callback errors stop the watcher
48
+ # The callback should handle its own errors
49
+ logger.exception("Exception during file watcher callback - continuing to watch")
@@ -0,0 +1,313 @@
1
+ """Interactive Q&A system using Matrix reactions as clickable buttons."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from contextlib import suppress
8
+ from typing import TYPE_CHECKING, NamedTuple
9
+
10
+ import nio
11
+
12
+ from .logging_config import get_logger
13
+ from .matrix.event_info import EventInfo
14
+ from .matrix.identity import is_agent_id
15
+
16
+ if TYPE_CHECKING:
17
+ from .config import Config
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class InteractiveQuestion(NamedTuple):
23
+ """Represents an active interactive question."""
24
+
25
+ room_id: str
26
+ thread_id: str | None
27
+ options: dict[str, str] # emoji/number -> value mapping
28
+ creator_agent: str
29
+
30
+
31
+ class InteractiveResponse(NamedTuple):
32
+ """Result of parsing and formatting an interactive response."""
33
+
34
+ formatted_text: str
35
+ option_map: dict[str, str] | None
36
+ options_list: list[dict[str, str]] | None
37
+
38
+
39
+ # Track active interactive questions by event_id
40
+ _active_questions: dict[str, InteractiveQuestion] = {}
41
+
42
+ # Constants
43
+ # Match interactive code blocks
44
+ INTERACTIVE_PATTERN = r"```(?:interactive\s*)?\n(?:interactive\s*\n)?(.*?)\n```"
45
+ MAX_OPTIONS = 5
46
+ DEFAULT_QUESTION = "Please choose an option:"
47
+ INSTRUCTION_TEXT = "React with an emoji or type the number to respond."
48
+
49
+
50
+ def should_create_interactive_question(response_text: str) -> bool:
51
+ """Check if the response contains an interactive question in JSON format.
52
+
53
+ Args:
54
+ response_text: The AI's response text
55
+
56
+ Returns:
57
+ True if an interactive code block is found
58
+
59
+ """
60
+ return bool(re.search(INTERACTIVE_PATTERN, response_text, re.DOTALL))
61
+
62
+
63
+ async def handle_reaction(
64
+ client: nio.AsyncClient,
65
+ event: nio.ReactionEvent,
66
+ agent_name: str,
67
+ config: Config,
68
+ ) -> tuple[str, str | None] | None:
69
+ """Handle a reaction event that might be an answer to a question.
70
+
71
+ Args:
72
+ client: The Matrix client
73
+ event: The reaction event
74
+ agent_name: The name of the agent handling this
75
+ config: Application configuration
76
+
77
+ Returns:
78
+ Tuple of (selected_value, thread_id) if this was a valid response, None otherwise
79
+
80
+ """
81
+ question = _active_questions.get(event.reacts_to)
82
+ if not question:
83
+ logger.debug(
84
+ "Reaction to unknown message",
85
+ reacts_to=event.reacts_to,
86
+ sender=event.sender,
87
+ reaction=event.key,
88
+ active_questions=list(_active_questions.keys()),
89
+ )
90
+ return None
91
+
92
+ # Only the agent who created the question should respond to reactions
93
+ if agent_name != question.creator_agent:
94
+ logger.debug(
95
+ "Ignoring reaction to question created by another agent",
96
+ reacting_agent=agent_name,
97
+ question_creator=question.creator_agent,
98
+ reaction=event.key,
99
+ )
100
+ return None
101
+
102
+ reaction_key = event.key
103
+ if reaction_key not in question.options:
104
+ return None
105
+
106
+ # Don't process our own reactions
107
+ if event.sender == client.user_id:
108
+ return None
109
+
110
+ # Ignore reactions from other agents
111
+ if is_agent_id(event.sender, config):
112
+ logger.debug("Ignoring reaction from agent", sender=event.sender, reaction=reaction_key)
113
+ return None
114
+
115
+ selected_value = question.options[reaction_key]
116
+
117
+ logger.info(
118
+ "Received answer via reaction",
119
+ user=event.sender,
120
+ reaction=reaction_key,
121
+ value=selected_value,
122
+ )
123
+
124
+ # Store the response for the agent to process
125
+ # The agent will continue the conversation based on this selection
126
+ # No confirmation message needed - the emoji reaction itself is the user's response
127
+
128
+ with suppress(KeyError):
129
+ del _active_questions[event.reacts_to]
130
+
131
+ # Return the selected value and thread_id so the agent can respond
132
+ return (selected_value, question.thread_id)
133
+
134
+
135
+ async def handle_text_response(
136
+ client: nio.AsyncClient,
137
+ room: nio.MatrixRoom,
138
+ event: nio.RoomMessageText,
139
+ agent_name: str,
140
+ ) -> tuple[str, str | None] | None:
141
+ """Handle text responses to interactive questions (e.g., "1", "2", "3").
142
+
143
+ Args:
144
+ client: The Matrix client
145
+ room: The room the message occurred in
146
+ event: The message event
147
+ agent_name: The name of the agent handling this
148
+
149
+ Returns:
150
+ Tuple of (selected_value, thread_id) if this was a valid response, None otherwise
151
+
152
+ """
153
+ message_text = event.body.strip()
154
+
155
+ # Look for numeric responses
156
+ if not message_text.isdigit() or len(message_text) > 1:
157
+ return None
158
+
159
+ thread_info = EventInfo.from_event(event.source)
160
+ thread_id = thread_info.thread_id
161
+
162
+ # Find matching active questions in this room/thread
163
+ for question_event_id, question in _active_questions.items():
164
+ if question.room_id != room.room_id:
165
+ continue
166
+ if question.thread_id != thread_id:
167
+ continue
168
+ if message_text not in question.options:
169
+ continue
170
+ if event.sender == client.user_id:
171
+ continue
172
+ # Only respond if this agent created the question
173
+ if agent_name != question.creator_agent:
174
+ continue
175
+
176
+ # Found a matching question
177
+ selected_value = question.options[message_text]
178
+
179
+ logger.info(
180
+ "Received answer via text",
181
+ user=event.sender,
182
+ text=message_text,
183
+ value=selected_value,
184
+ )
185
+
186
+ del _active_questions[question_event_id]
187
+
188
+ return (selected_value, question.thread_id)
189
+
190
+ return None
191
+
192
+
193
+ def parse_and_format_interactive(response_text: str, extract_mapping: bool = False) -> InteractiveResponse:
194
+ """Parse and format interactive content from response text.
195
+
196
+ Args:
197
+ response_text: The response text containing interactive JSON
198
+ extract_mapping: Whether to extract option mapping and return options list
199
+
200
+ Returns:
201
+ InteractiveResponse with formatted_text, option_map, and options_list
202
+
203
+ """
204
+ # Find the first interactive block for processing
205
+ first_match = re.search(INTERACTIVE_PATTERN, response_text, re.DOTALL)
206
+
207
+ if not first_match:
208
+ return InteractiveResponse(response_text, None, None)
209
+
210
+ try:
211
+ interactive_data = json.loads(first_match.group(1))
212
+ except json.JSONDecodeError:
213
+ return InteractiveResponse(response_text, None, None)
214
+
215
+ question = interactive_data.get("question", DEFAULT_QUESTION)
216
+ options = interactive_data.get("options", [])
217
+
218
+ if not options:
219
+ return InteractiveResponse(response_text, None, None)
220
+
221
+ options = options[:MAX_OPTIONS]
222
+ clean_response = response_text.replace(first_match.group(0), "").strip()
223
+
224
+ option_lines = []
225
+ option_map: dict[str, str] | None = {} if extract_mapping else None
226
+
227
+ for i, opt in enumerate(options, 1):
228
+ emoji_char = opt.get("emoji", "โ“")
229
+ label = opt.get("label", "Option")
230
+ option_lines.append(f"{i}. {emoji_char} {label}")
231
+
232
+ if extract_mapping and option_map is not None:
233
+ value = opt.get("value", label.lower())
234
+ option_map[emoji_char] = value
235
+ option_map[str(i)] = value
236
+
237
+ # Combine everything into the final message
238
+ message_parts = []
239
+ if clean_response:
240
+ message_parts.append(clean_response)
241
+ message_parts.append("") # Empty line
242
+ message_parts.append(question)
243
+ message_parts.append("") # Empty line
244
+ message_parts.extend(option_lines)
245
+ message_parts.append("") # Empty line
246
+ message_parts.append(INSTRUCTION_TEXT)
247
+
248
+ final_text = "\n".join(message_parts)
249
+
250
+ return InteractiveResponse(final_text, option_map, options if extract_mapping else None)
251
+
252
+
253
+ def register_interactive_question(
254
+ event_id: str,
255
+ room_id: str,
256
+ thread_id: str | None,
257
+ option_map: dict[str, str],
258
+ agent_name: str,
259
+ ) -> None:
260
+ """Register an interactive question for tracking.
261
+
262
+ Args:
263
+ event_id: The event ID of the message with the question
264
+ room_id: The room ID
265
+ thread_id: Thread ID if in a thread
266
+ option_map: Mapping of emoji/number to values
267
+ agent_name: The agent that created the question
268
+
269
+ """
270
+ _active_questions[event_id] = InteractiveQuestion(
271
+ room_id=room_id,
272
+ thread_id=thread_id,
273
+ options=option_map,
274
+ creator_agent=agent_name,
275
+ )
276
+ logger.info("Registered interactive question", event_id=event_id, options=len(option_map))
277
+
278
+
279
+ async def add_reaction_buttons(
280
+ client: nio.AsyncClient,
281
+ room_id: str,
282
+ event_id: str,
283
+ options: list[dict[str, str]],
284
+ ) -> None:
285
+ """Add reaction buttons to a message.
286
+
287
+ Args:
288
+ client: The Matrix client
289
+ room_id: The room ID
290
+ event_id: The event ID of the message to add reactions to
291
+ options: List of option dictionaries with 'emoji' keys
292
+
293
+ """
294
+ for opt in options:
295
+ emoji_char = opt.get("emoji", "โ“")
296
+ reaction_response = await client.room_send(
297
+ room_id=room_id,
298
+ message_type="m.reaction",
299
+ content={
300
+ "m.relates_to": {
301
+ "rel_type": "m.annotation",
302
+ "event_id": event_id,
303
+ "key": emoji_char,
304
+ },
305
+ },
306
+ )
307
+ if not isinstance(reaction_response, nio.RoomSendResponse):
308
+ logger.warning("Failed to add reaction", emoji=emoji_char, error=str(reaction_response))
309
+
310
+
311
+ def cleanup() -> None:
312
+ """Clean up when shutting down."""
313
+ _active_questions.clear()
@@ -0,0 +1,207 @@
1
+ """Logging configuration for mindroom using structlog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import logging
7
+ import logging.config
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+
11
+ import structlog
12
+
13
+ from mindroom.constants import STORAGE_PATH
14
+
15
+ __all__ = ["emoji", "get_logger", "setup_logging"]
16
+
17
+
18
+ class NioValidationFilter(logging.Filter):
19
+ """Filter out harmless nio validation warnings that confuse AI agents."""
20
+
21
+ def filter(self, record: logging.LogRecord) -> bool:
22
+ """Filter out specific nio validation warnings.
23
+
24
+ Returns:
25
+ False to suppress the log record, True to keep it
26
+
27
+ """
28
+ # Filter out only the specific user_id and room_id validation warnings from nio
29
+ if record.name == "nio.responses":
30
+ msg = record.getMessage()
31
+ if "Error validating response: 'user_id' is a required property" in msg:
32
+ # This warning occurs when Matrix server responses don't include user_id
33
+ # which happens during registration checks. It's harmless.
34
+ return False
35
+ if "Error validating response: 'room_id' is a required property" in msg:
36
+ # Similar harmless warning for room_id
37
+ return False
38
+ return True
39
+
40
+
41
+ def emoji(agent_name: str) -> str:
42
+ """Get an emoji-prefixed agent name string with consistent emoji based on the name.
43
+
44
+ Args:
45
+ agent_name: The agent name to add emoji to
46
+
47
+ Returns:
48
+ The agent name with a unique emoji prefix
49
+
50
+ """
51
+ # Emojis for different agents
52
+ emojis = [
53
+ "๐Ÿค–", # robot
54
+ "๐Ÿงฎ", # abacus
55
+ "๐Ÿ’ก", # light bulb
56
+ "๐Ÿ”ง", # wrench
57
+ "๐Ÿ“Š", # chart
58
+ "๐ŸŽฏ", # target
59
+ "๐Ÿš€", # rocket
60
+ "โšก", # lightning
61
+ "๐Ÿ”", # magnifying glass
62
+ "๐Ÿ“", # memo
63
+ "๐ŸŽจ", # artist palette
64
+ "๐Ÿงช", # test tube
65
+ "๐ŸŽช", # circus tent
66
+ "๐ŸŒŸ", # star
67
+ "๐Ÿ”ฎ", # crystal ball
68
+ "๐Ÿ› ๏ธ", # hammer and wrench
69
+ ]
70
+
71
+ # Use hash to get consistent emoji for each agent
72
+ hash_value = int(hashlib.sha256(agent_name.encode()).hexdigest(), 16)
73
+ emoji_index = hash_value % len(emojis)
74
+ emoji = emojis[emoji_index]
75
+
76
+ return f"{emoji} {agent_name}"
77
+
78
+
79
+ def setup_logging(level: str = "INFO") -> None:
80
+ """Configure structlog for mindroom with file and console output.
81
+
82
+ Args:
83
+ level: Minimum logging level (e.g., "DEBUG", "INFO", "WARNING", "ERROR")
84
+
85
+ """
86
+ # Create logs directory if it doesn't exist
87
+ logs_dir = Path(STORAGE_PATH) / "logs"
88
+ logs_dir.mkdir(exist_ok=True, parents=True)
89
+
90
+ # Create timestamped log file
91
+ timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
92
+ log_file = logs_dir / f"mindroom_{timestamp}.log"
93
+
94
+ # Shared processors that don't affect output format
95
+ timestamper = structlog.processors.TimeStamper(fmt="iso")
96
+ pre_chain = [
97
+ structlog.stdlib.add_log_level,
98
+ timestamper,
99
+ ]
100
+
101
+ # Configure logging with both console and file handlers
102
+ logging.config.dictConfig(
103
+ {
104
+ "version": 1,
105
+ "disable_existing_loggers": False,
106
+ "formatters": {
107
+ "plain": {
108
+ "()": structlog.stdlib.ProcessorFormatter,
109
+ "processors": [
110
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
111
+ structlog.dev.ConsoleRenderer(colors=False),
112
+ ],
113
+ "foreign_pre_chain": pre_chain,
114
+ },
115
+ "colored": {
116
+ "()": structlog.stdlib.ProcessorFormatter,
117
+ "processors": [
118
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
119
+ structlog.dev.ConsoleRenderer(
120
+ colors=True,
121
+ exception_formatter=structlog.dev.RichTracebackFormatter(
122
+ # The locals can be very large, so we hide them by default
123
+ show_locals=False,
124
+ ),
125
+ ),
126
+ ],
127
+ "foreign_pre_chain": pre_chain,
128
+ },
129
+ },
130
+ "filters": {
131
+ "nio_validation": {
132
+ "()": NioValidationFilter,
133
+ },
134
+ },
135
+ "handlers": {
136
+ "console": {
137
+ "level": level.upper(),
138
+ "class": "logging.StreamHandler",
139
+ "stream": "ext://sys.stderr",
140
+ "formatter": "colored",
141
+ "filters": ["nio_validation"],
142
+ },
143
+ "file": {
144
+ "level": level.upper(),
145
+ "class": "logging.FileHandler",
146
+ "filename": str(log_file),
147
+ "mode": "a",
148
+ "encoding": "utf-8",
149
+ "formatter": "plain",
150
+ "filters": ["nio_validation"],
151
+ },
152
+ },
153
+ "loggers": {
154
+ "": { # Root logger
155
+ "handlers": ["console", "file"],
156
+ "level": level.upper(),
157
+ "propagate": False,
158
+ },
159
+ # Reduce verbosity of nio (Matrix) library
160
+ "nio": {
161
+ "level": "WARNING",
162
+ },
163
+ "nio.client": {
164
+ "level": "WARNING",
165
+ },
166
+ "nio.responses": {
167
+ "level": "WARNING",
168
+ },
169
+ },
170
+ },
171
+ )
172
+
173
+ # Configure structlog to use stdlib logging
174
+ structlog.configure(
175
+ processors=[
176
+ structlog.contextvars.merge_contextvars,
177
+ structlog.stdlib.filter_by_level,
178
+ structlog.stdlib.add_logger_name,
179
+ structlog.stdlib.add_log_level,
180
+ timestamper,
181
+ structlog.stdlib.PositionalArgumentsFormatter(),
182
+ structlog.processors.StackInfoRenderer(),
183
+ structlog.processors.UnicodeDecoder(),
184
+ structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
185
+ ],
186
+ context_class=dict,
187
+ logger_factory=structlog.stdlib.LoggerFactory(),
188
+ wrapper_class=structlog.stdlib.BoundLogger,
189
+ cache_logger_on_first_use=True,
190
+ )
191
+
192
+ # Log startup message
193
+ logger = get_logger(__name__)
194
+ logger.info("Logging initialized", log_file=str(log_file), level=level)
195
+
196
+
197
+ def get_logger(name: str = __name__) -> structlog.stdlib.BoundLogger:
198
+ """Get a structlog logger instance.
199
+
200
+ Args:
201
+ name: Logger name (typically __name__)
202
+
203
+ Returns:
204
+ Configured structlog logger
205
+
206
+ """
207
+ return structlog.get_logger(name) # type: ignore[no-any-return]
@@ -0,0 +1 @@
1
+ """Matrix operations module for mindroom."""