Skip to content

Commit 7d06576

Browse files
committed
Release v4.4.12
1 parent ab80807 commit 7d06576

27 files changed

+1576
-282
lines changed

docker/Dockerfile.chat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=4.4.11" \
19+
"praisonai>=4.4.12" \
2020
"praisonai[chat]" \
2121
"embedchain[github,youtube]"
2222

docker/Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
2020
# Install Python packages (using latest versions)
2121
RUN pip install --no-cache-dir \
2222
praisonai_tools \
23-
"praisonai>=4.4.11" \
23+
"praisonai>=4.4.12" \
2424
"praisonai[ui]" \
2525
"praisonai[chat]" \
2626
"praisonai[realtime]" \

docker/Dockerfile.ui

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
1616
# Install Python packages (using latest versions)
1717
RUN pip install --no-cache-dir \
1818
praisonai_tools \
19-
"praisonai>=4.4.11" \
19+
"praisonai>=4.4.12" \
2020
"praisonai[ui]" \
2121
"praisonai[crewai]"
2222

src/praisonai-agents/praisonaiagents/gateway/protocols.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ class EventType(str, Enum):
5353
MESSAGE_ACK = "message_ack"
5454
TYPING = "typing"
5555

56+
# Streaming events (relayed from agent's StreamEventEmitter)
57+
TOKEN_STREAM = "token_stream"
58+
TOOL_CALL_STREAM = "tool_call_stream"
59+
STREAM_END = "stream_end"
60+
5661
# System events
5762
HEALTH = "health"
5863
ERROR = "error"

src/praisonai-agents/praisonaiagents/hooks/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ def my_hook(event_data):
7171
"SessionEndInput",
7272
"OnErrorInput",
7373
"OnRetryInput",
74+
# Message lifecycle event inputs
75+
"MessageReceivedInput",
76+
"MessageSendingInput",
77+
"MessageSentInput",
7478
# Middleware types
7579
"InvocationContext",
7680
"ModelRequest",
@@ -130,6 +134,9 @@ def my_hook(event_data):
130134
'SessionEndInput': ('praisonaiagents.hooks.events', 'SessionEndInput'),
131135
'OnErrorInput': ('praisonaiagents.hooks.events', 'OnErrorInput'),
132136
'OnRetryInput': ('praisonaiagents.hooks.events', 'OnRetryInput'),
137+
'MessageReceivedInput': ('praisonaiagents.hooks.events', 'MessageReceivedInput'),
138+
'MessageSendingInput': ('praisonaiagents.hooks.events', 'MessageSendingInput'),
139+
'MessageSentInput': ('praisonaiagents.hooks.events', 'MessageSentInput'),
133140
},
134141
'middleware_types': {
135142
'InvocationContext': ('praisonaiagents.hooks.middleware', 'InvocationContext'),

src/praisonai-agents/praisonaiagents/hooks/events.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,68 @@ def to_dict(self) -> Dict[str, Any]:
193193
"operation": self.operation
194194
})
195195
return base
196+
197+
198+
@dataclass
199+
class MessageReceivedInput(HookInput):
200+
"""Input for MESSAGE_RECEIVED hooks (bot received an incoming message)."""
201+
platform: str = ""
202+
content: str = ""
203+
sender_id: str = ""
204+
channel_id: str = ""
205+
channel_type: str = ""
206+
message_id: str = ""
207+
208+
def to_dict(self) -> Dict[str, Any]:
209+
base = super().to_dict()
210+
base.update({
211+
"platform": self.platform,
212+
"content": self.content[:500] if self.content else "",
213+
"sender_id": self.sender_id,
214+
"channel_id": self.channel_id,
215+
"channel_type": self.channel_type,
216+
"message_id": self.message_id,
217+
})
218+
return base
219+
220+
221+
@dataclass
222+
class MessageSendingInput(HookInput):
223+
"""Input for MESSAGE_SENDING hooks (bot about to send a message).
224+
225+
Hooks can modify content or cancel the send by returning
226+
HookResult with decision=DENY.
227+
"""
228+
platform: str = ""
229+
content: str = ""
230+
channel_id: str = ""
231+
reply_to: Optional[str] = None
232+
233+
def to_dict(self) -> Dict[str, Any]:
234+
base = super().to_dict()
235+
base.update({
236+
"platform": self.platform,
237+
"content": self.content[:500] if self.content else "",
238+
"channel_id": self.channel_id,
239+
"reply_to": self.reply_to,
240+
})
241+
return base
242+
243+
244+
@dataclass
245+
class MessageSentInput(HookInput):
246+
"""Input for MESSAGE_SENT hooks (bot successfully sent a message)."""
247+
platform: str = ""
248+
content: str = ""
249+
channel_id: str = ""
250+
message_id: str = ""
251+
252+
def to_dict(self) -> Dict[str, Any]:
253+
base = super().to_dict()
254+
base.update({
255+
"platform": self.platform,
256+
"content": self.content[:500] if self.content else "",
257+
"channel_id": self.channel_id,
258+
"message_id": self.message_id,
259+
})
260+
return base

src/praisonai-agents/praisonaiagents/hooks/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(
7070
default_timeout: Default timeout for hooks
7171
cwd: Working directory for command hooks
7272
"""
73-
self._registry = registry or HookRegistry()
73+
self._registry = registry if registry is not None else HookRegistry()
7474
self._default_timeout = default_timeout
7575
self._cwd = cwd or os.getcwd()
7676

src/praisonai-agents/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "praisonaiagents"
7-
version = "1.4.7"
7+
version = "1.4.8"
88
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/praisonai-agents/tests/integration/test_whatsapp_web_real.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"""
1313

1414
import asyncio
15-
import json
1615
import os
1716
import signal
1817
import subprocess
@@ -154,13 +153,17 @@ def test_web_mode_experimental_warning(self):
154153
proc.kill()
155154
stdout, stderr = proc.communicate()
156155
combined = stdout + stderr
157-
# The warning is printed by the CLI before start
156+
# The warning is printed by the CLI before start.
157+
# Once the event loop fix landed, neonize actually connects
158+
# to WhatsApp servers, so we may also see websocket messages.
158159
assert (
159160
"experimental" in combined.lower()
160161
or "EXPERIMENTAL" in combined
161162
or "web mode" in combined.lower()
162163
or "Starting WhatsApp" in combined
163-
), f"Expected experimental warning in output, got: {combined[:500]}"
164+
or "websocket" in combined.lower()
165+
or "whatsapp" in combined.lower()
166+
), f"Expected WhatsApp output, got: {combined[:500]}"
164167
except Exception:
165168
proc.kill()
166169
proc.wait()
@@ -430,6 +433,48 @@ async def patched_connect(self_client):
430433
finally:
431434
NewAClient.connect = original_connect
432435

436+
@pytest.mark.asyncio
437+
async def test_adapter_connect_bridges_event_loop(self):
438+
"""connect() patches neonize's event_global_loop with the running loop.
439+
440+
Root cause fix: neonize creates event_global_loop = asyncio.new_event_loop()
441+
at import time but never starts it. Go-thread callbacks (QR, messages)
442+
are posted to that dead loop. Our adapter must replace it with the
443+
currently running loop so callbacks actually execute.
444+
"""
445+
sys.path.insert(0, PRAISONAI_SRC)
446+
from praisonai.bots._whatsapp_web_adapter import WhatsAppWebAdapter
447+
from neonize.aioze.client import NewAClient
448+
import neonize.aioze.events as nz_events
449+
import neonize.aioze.client as nz_client
450+
451+
tmpdir = tempfile.mkdtemp()
452+
adapter = WhatsAppWebAdapter(creds_dir=tmpdir)
453+
454+
running_loop = asyncio.get_running_loop()
455+
456+
# Before connect: neonize's loop is NOT our running loop
457+
original_nz_loop = nz_events.event_global_loop
458+
459+
original_connect = NewAClient.connect
460+
async def patched_connect(self_client):
461+
raise ConnectionError("test: skip real connection")
462+
463+
NewAClient.connect = patched_connect
464+
try:
465+
with pytest.raises(ConnectionError):
466+
await adapter.connect()
467+
468+
# After connect: neonize's event loops must be bridged to ours
469+
assert nz_events.event_global_loop is running_loop
470+
assert nz_client.event_global_loop is running_loop
471+
assert adapter._client.loop is running_loop
472+
finally:
473+
NewAClient.connect = original_connect
474+
# Restore original loop to avoid test pollution
475+
nz_events.event_global_loop = original_nz_loop
476+
nz_client.event_global_loop = original_nz_loop
477+
433478
def test_adapter_logout_cleans_db(self):
434479
"""logout() removes the DB file."""
435480
sys.path.insert(0, PRAISONAI_SRC)

0 commit comments

Comments
 (0)