mindbot 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.
mindbot/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """MindBot - AI Assistant powered by Thryve."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __logo__ = """
6
+ ╔════════════════════════════════════╗
7
+ ║ MindBot ║
8
+ ╚════════════════════════════════════╝
9
+ """
10
+
11
+ from mindbot.core.bot import MindBot
12
+ from mindbot.core.config import MindConfig
13
+ from mindbot.bus import MessageBus, InboundMessage, OutboundMessage
14
+ from mindbot.channels import BaseChannel, ChannelManager
15
+
16
+ __all__ = [
17
+ "MindBot",
18
+ "MindConfig",
19
+ "MessageBus",
20
+ "InboundMessage",
21
+ "OutboundMessage",
22
+ "BaseChannel",
23
+ "ChannelManager",
24
+ "__version__",
25
+ "__logo__",
26
+ ]
mindbot/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Entry point for running mindbot as a module: python -m mindbot"""
2
+
3
+ from mindbot.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,10 @@
1
+ """Message bus for channel-agent communication."""
2
+
3
+ from mindbot.bus.events import InboundMessage, OutboundMessage
4
+ from mindbot.bus.queue import MessageBus
5
+
6
+ __all__ = [
7
+ "InboundMessage",
8
+ "OutboundMessage",
9
+ "MessageBus",
10
+ ]
mindbot/bus/events.py ADDED
@@ -0,0 +1,35 @@
1
+ """Event types for the message bus."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ @dataclass
9
+ class InboundMessage:
10
+ """Message received from a chat channel."""
11
+
12
+ channel: str # telegram, discord, http, cli
13
+ sender_id: str # User identifier
14
+ chat_id: str # Chat/channel identifier
15
+ content: str # Message text
16
+ timestamp: datetime = field(default_factory=datetime.now)
17
+ media: list[str] = field(default_factory=list) # Media URLs
18
+ metadata: dict[str, Any] = field(default_factory=dict) # Channel-specific data
19
+
20
+ @property
21
+ def session_key(self) -> str:
22
+ """Unique key for session identification."""
23
+ return f"{self.channel}:{self.chat_id}"
24
+
25
+
26
+ @dataclass
27
+ class OutboundMessage:
28
+ """Message to send to a chat channel."""
29
+
30
+ channel: str
31
+ chat_id: str
32
+ content: str
33
+ reply_to: str | None = None
34
+ media: list[str] = field(default_factory=list)
35
+ metadata: dict[str, Any] = field(default_factory=dict)
mindbot/bus/queue.py ADDED
@@ -0,0 +1,80 @@
1
+ """Async message queue for decoupled channel-agent communication."""
2
+
3
+ import asyncio
4
+ from typing import Callable, Awaitable
5
+
6
+ from loguru import logger
7
+
8
+ from mindbot.bus.events import InboundMessage, OutboundMessage
9
+
10
+
11
+ class MessageBus:
12
+ """Async message bus that decouples chat channels from the agent core.
13
+
14
+ Channels push messages to the inbound queue, and the agent processes
15
+ them and pushes responses to the outbound queue.
16
+ """
17
+
18
+ def __init__(self):
19
+ self.inbound: asyncio.Queue[InboundMessage] = asyncio.Queue()
20
+ self.outbound: asyncio.Queue[OutboundMessage] = asyncio.Queue()
21
+ self._outbound_subscribers: dict[str, list[Callable[[OutboundMessage], Awaitable[None]]]] = {}
22
+ self._running = False
23
+
24
+ async def publish_inbound(self, msg: InboundMessage) -> None:
25
+ """Publish a message from a channel to the agent."""
26
+ await self.inbound.put(msg)
27
+
28
+ async def consume_inbound(self) -> InboundMessage:
29
+ """Consume the next inbound message (blocks until available)."""
30
+ return await self.inbound.get()
31
+
32
+ async def publish_outbound(self, msg: OutboundMessage) -> None:
33
+ """Publish a response from the agent to channels."""
34
+ await self.outbound.put(msg)
35
+
36
+ async def consume_outbound(self) -> OutboundMessage:
37
+ """Consume the next outbound message (blocks until available)."""
38
+ return await self.outbound.get()
39
+
40
+ def subscribe_outbound(
41
+ self,
42
+ channel: str,
43
+ callback: Callable[[OutboundMessage], Awaitable[None]]
44
+ ) -> None:
45
+ """Subscribe to outbound messages for a specific channel."""
46
+ if channel not in self._outbound_subscribers:
47
+ self._outbound_subscribers[channel] = []
48
+ self._outbound_subscribers[channel].append(callback)
49
+
50
+ async def dispatch_outbound(self) -> None:
51
+ """Dispatch outbound messages to subscribed channels.
52
+
53
+ Run this as a background task.
54
+ """
55
+ self._running = True
56
+ while self._running:
57
+ try:
58
+ msg = await asyncio.wait_for(self.outbound.get(), timeout=1.0)
59
+ subscribers = self._outbound_subscribers.get(msg.channel, [])
60
+ for callback in subscribers:
61
+ try:
62
+ await callback(msg)
63
+ except Exception as e:
64
+ logger.error(f"Error dispatching to {msg.channel}: {e}")
65
+ except asyncio.TimeoutError:
66
+ continue
67
+
68
+ def stop(self) -> None:
69
+ """Stop the dispatcher loop."""
70
+ self._running = False
71
+
72
+ @property
73
+ def inbound_size(self) -> int:
74
+ """Number of pending inbound messages."""
75
+ return self.inbound.qsize()
76
+
77
+ @property
78
+ def outbound_size(self) -> int:
79
+ """Number of pending outbound messages."""
80
+ return self.outbound.qsize()
@@ -0,0 +1,11 @@
1
+ """Chat channels for MindBot."""
2
+
3
+ from mindbot.channels.base import BaseChannel
4
+ from mindbot.channels.manager import ChannelManager
5
+ from mindbot.channels.feishu import FeishuChannel
6
+
7
+ __all__ = [
8
+ "BaseChannel",
9
+ "ChannelManager",
10
+ "FeishuChannel",
11
+ ]
@@ -0,0 +1,121 @@
1
+ """Base channel interface for chat platforms."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+ from loguru import logger
7
+
8
+ from mindbot.bus.events import InboundMessage, OutboundMessage
9
+ from mindbot.bus.queue import MessageBus
10
+
11
+
12
+ class BaseChannel(ABC):
13
+ """Abstract base class for chat channel implementations.
14
+
15
+ Each channel (Telegram, Discord, etc.) should implement this interface
16
+ to integrate with the mindbot message bus.
17
+ """
18
+
19
+ name: str = "base"
20
+
21
+ def __init__(self, config: Any, bus: MessageBus):
22
+ """Initialize the channel.
23
+
24
+ Args:
25
+ config: Channel-specific configuration.
26
+ bus: The message bus for communication.
27
+ """
28
+ self.config = config
29
+ self.bus = bus
30
+ self._running = False
31
+
32
+ @abstractmethod
33
+ async def start(self) -> None:
34
+ """Start the channel and begin listening for messages.
35
+
36
+ This should be a long-running async task that:
37
+ 1. Connects to the chat platform
38
+ 2. Listens for incoming messages
39
+ 3. Forwards messages to the bus via _handle_message()
40
+ """
41
+ pass
42
+
43
+ @abstractmethod
44
+ async def stop(self) -> None:
45
+ """Stop the channel and clean up resources."""
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def send(self, msg: OutboundMessage) -> None:
50
+ """Send a message through this channel.
51
+
52
+ Args:
53
+ msg: The message to send.
54
+ """
55
+ pass
56
+
57
+ def is_allowed(self, sender_id: str) -> bool:
58
+ """Check if a sender is allowed to use this bot.
59
+
60
+ Args:
61
+ sender_id: The sender's identifier.
62
+
63
+ Returns:
64
+ True if allowed, False otherwise.
65
+ """
66
+ allow_list = getattr(self.config, "allow_from", [])
67
+
68
+ # If no allow list, allow everyone
69
+ if not allow_list:
70
+ return True
71
+
72
+ sender_str = str(sender_id)
73
+ if sender_str in allow_list:
74
+ return True
75
+ if "|" in sender_str:
76
+ for part in sender_str.split("|"):
77
+ if part and part in allow_list:
78
+ return True
79
+ return False
80
+
81
+ async def _handle_message(
82
+ self,
83
+ sender_id: str,
84
+ chat_id: str,
85
+ content: str,
86
+ media: list[str] | None = None,
87
+ metadata: dict[str, Any] | None = None
88
+ ) -> None:
89
+ """Handle an incoming message from the chat platform.
90
+
91
+ This method checks permissions and forwards to the bus.
92
+
93
+ Args:
94
+ sender_id: The sender's identifier.
95
+ chat_id: The chat/channel identifier.
96
+ content: Message text content.
97
+ media: Optional list of media URLs.
98
+ metadata: Optional channel-specific metadata.
99
+ """
100
+ if not self.is_allowed(sender_id):
101
+ logger.warning(
102
+ f"Access denied for sender {sender_id} on channel {self.name}. "
103
+ f"Add them to allow_from list in config to grant access."
104
+ )
105
+ return
106
+
107
+ msg = InboundMessage(
108
+ channel=self.name,
109
+ sender_id=str(sender_id),
110
+ chat_id=str(chat_id),
111
+ content=content,
112
+ media=media or [],
113
+ metadata=metadata or {}
114
+ )
115
+
116
+ await self.bus.publish_inbound(msg)
117
+
118
+ @property
119
+ def is_running(self) -> bool:
120
+ """Check if the channel is running."""
121
+ return self._running
@@ -0,0 +1,71 @@
1
+ """CLI channel for MindBot."""
2
+
3
+ import asyncio
4
+ from typing import Any
5
+
6
+ from loguru import logger
7
+
8
+ from mindbot.bus.events import OutboundMessage
9
+ from mindbot.bus.queue import MessageBus
10
+ from mindbot.channels.base import BaseChannel
11
+
12
+
13
+ class CLIChannel(BaseChannel):
14
+ """CLI (stdin/stdout) channel for interactive terminal use.
15
+
16
+ This channel handles interactive command-line conversations.
17
+ """
18
+
19
+ name: str = "cli"
20
+
21
+ def __init__(self, config: Any, bus: MessageBus):
22
+ super().__init__(config, bus)
23
+ self._input_task: asyncio.Task | None = None
24
+
25
+ async def start(self) -> None:
26
+ """Start the CLI channel."""
27
+ self._running = True
28
+ self._input_task = asyncio.create_task(self._read_input())
29
+ logger.info("CLI channel started")
30
+
31
+ async def stop(self) -> None:
32
+ """Stop the CLI channel."""
33
+ self._running = False
34
+ if self._input_task:
35
+ self._input_task.cancel()
36
+ try:
37
+ await self._input_task
38
+ except asyncio.CancelledError:
39
+ pass
40
+ logger.info("CLI channel stopped")
41
+
42
+ async def _read_input(self) -> None:
43
+ """Read input from stdin and send to message bus."""
44
+ loop = asyncio.get_event_loop()
45
+
46
+ while self._running:
47
+ try:
48
+ # Read line from stdin
49
+ line = await loop.run_in_executor(None, input, ">>> ")
50
+
51
+ if not line.strip():
52
+ continue
53
+
54
+ if line.strip().lower() in ["exit", "quit", "bye"]:
55
+ break
56
+
57
+ await self._handle_message(
58
+ sender_id="cli_user",
59
+ chat_id="cli",
60
+ content=line.strip(),
61
+ metadata={"session_id": "default"}
62
+ )
63
+
64
+ except EOFError:
65
+ break
66
+ except Exception as e:
67
+ logger.error(f"Error reading CLI input: {e}")
68
+
69
+ async def send(self, msg: OutboundMessage) -> None:
70
+ """Send a message to stdout."""
71
+ print(f"\n{msg.content}\n>>> ", end="")