mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ac4414901 | ||
|
|
597280a6c6 | ||
|
|
fe954cbd4a | ||
|
|
ab9ef2544d | ||
|
|
e15b3c0718 | ||
|
|
12b42e88be | ||
|
|
5e80f11535 | ||
|
|
84ae1fd0ac | ||
|
|
b59af41d88 | ||
|
|
8e5c4d40e2 |
No files matched your search
@@ -13,7 +13,9 @@ MEETING_AGENT_PORT=8000
|
||||
# ── Recall.ai (EU workspace: Praison) ───────────────────────────────────────
|
||||
# Region must match dashboard: eu-central-1 (Frankfurt).
|
||||
RECALL_REGION=eu-central-1
|
||||
RECALL_WORKSPACE_ID=33a035c7-03c1-4ec9-b752-18cdfbbefc42
|
||||
# Optional: your Recall workspace ID (shown in the config-status API only; the
|
||||
# API key already scopes requests to a workspace).
|
||||
RECALL_WORKSPACE_ID=
|
||||
|
||||
# REST API key from Recall dashboard (Developers → API Keys). Server-side only.
|
||||
RECALL_API_KEY=
|
||||
|
||||
@@ -17,9 +17,10 @@ Meeting Bots for live Zoom / Meet / Teams capture.
|
||||
|
||||
## Recall.ai workspace
|
||||
|
||||
This integration targets the **Praison** workspace in **EU (Frankfurt)**:
|
||||
This integration targets your Recall workspace in **EU (Frankfurt)** by default:
|
||||
|
||||
- Workspace ID: `33a035c7-03c1-4ec9-b752-18cdfbbefc42`
|
||||
- Workspace ID: set `RECALL_WORKSPACE_ID` to your own (optional — the API key
|
||||
already scopes requests to a workspace)
|
||||
- API region: `eu-central-1`
|
||||
|
||||
Follow the [Recall onboarding guide](https://docs.recall.ai/docs/agent-quickstarts) via Recall MCP (`recall://guides/onboarding`) for credential and webhook setup.
|
||||
|
||||
@@ -102,7 +102,7 @@ _agent: Any | None = None
|
||||
def _recall_schedule_tool():
|
||||
from praisonai_tools.tools.decorator import tool
|
||||
|
||||
from integrations.recall.service import schedule_recall_bot as _schedule
|
||||
from meeting_integrations.recall.service import schedule_recall_bot as _schedule
|
||||
|
||||
@tool
|
||||
def schedule_recall_bot(meeting_url: str, title: str = "") -> dict:
|
||||
@@ -115,7 +115,7 @@ def _recall_schedule_tool():
|
||||
def _upcoming_meetings_tool():
|
||||
from praisonai_tools.tools.decorator import tool
|
||||
|
||||
from integrations.recall.calendar_service import list_upcoming_calendar_meetings
|
||||
from meeting_integrations.recall.calendar_service import list_upcoming_calendar_meetings
|
||||
|
||||
@tool
|
||||
def get_upcoming_meetings(hours: int = 24) -> list[dict]:
|
||||
@@ -194,8 +194,8 @@ def _sync_stale_recall_status(record: dict[str, Any]) -> dict[str, Any]:
|
||||
if not os.getenv("RECALL_API_KEY"):
|
||||
return record
|
||||
try:
|
||||
from integrations.recall.client import RecallClient
|
||||
from integrations.recall.config import load_recall_settings
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.config import load_recall_settings
|
||||
from pipeline import merge_meeting_metadata
|
||||
|
||||
bot = RecallClient(load_recall_settings()).get_bot(str(bot_id))
|
||||
@@ -464,7 +464,7 @@ async def calendar_page():
|
||||
status_note = "Set RECALL_API_KEY to load calendar status."
|
||||
if os.getenv("RECALL_API_KEY"):
|
||||
try:
|
||||
from integrations.recall.calendar_service import list_upcoming_calendar_meetings
|
||||
from meeting_integrations.recall.calendar_service import list_upcoming_calendar_meetings
|
||||
|
||||
upcoming = list_upcoming_calendar_meetings(hours=48)
|
||||
status_note = f"{len(upcoming)} upcoming event(s) with video links (48h window)."
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Recall.ai Meeting Bot integration for the meeting-agent example."""
|
||||
|
||||
from integrations.recall.client import RecallClient
|
||||
from integrations.recall.config import RecallSettings, load_recall_settings
|
||||
from integrations.recall.verify import VerificationError, verify_request_from_recall
|
||||
|
||||
__all__ = [
|
||||
"RecallClient",
|
||||
"RecallSettings",
|
||||
"VerificationError",
|
||||
"load_recall_settings",
|
||||
"verify_request_from_recall",
|
||||
]
|
||||
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
"""Recall.ai Meeting Bot integration for the meeting-agent example."""
|
||||
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.config import RecallSettings, load_recall_settings
|
||||
from meeting_integrations.recall.verify import VerificationError, verify_request_from_recall
|
||||
|
||||
__all__ = [
|
||||
"RecallClient",
|
||||
"RecallSettings",
|
||||
"VerificationError",
|
||||
"load_recall_settings",
|
||||
"verify_request_from_recall",
|
||||
]
|
||||
File renamed without changes.
+4
-4
@@ -6,9 +6,9 @@ import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from integrations.recall.client import RecallAPIError, RecallClient
|
||||
from integrations.recall.config import RecallSettings
|
||||
from integrations.recall.store import RecallStore
|
||||
from meeting_integrations.recall.client import RecallAPIError, RecallClient
|
||||
from meeting_integrations.recall.config import RecallSettings
|
||||
from meeting_integrations.recall.store import RecallStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,7 +64,7 @@ def list_upcoming_calendar_meetings(
|
||||
client: RecallClient | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List upcoming Recall calendar events with video links (for UI + agent tool)."""
|
||||
from integrations.recall.config import load_recall_settings
|
||||
from meeting_integrations.recall.config import load_recall_settings
|
||||
|
||||
settings = settings or load_recall_settings()
|
||||
client = client or RecallClient(settings)
|
||||
+1
-1
@@ -10,7 +10,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from integrations.recall.config import RecallSettings
|
||||
from meeting_integrations.recall.config import RecallSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+1
-3
@@ -93,9 +93,7 @@ def load_recall_settings(*, require_public_url: bool = False) -> RecallSettings:
|
||||
api_key=_require("RECALL_API_KEY"),
|
||||
webhook_verification_secret=_require("RECALL_WEBHOOK_VERIFICATION_SECRET"),
|
||||
public_api_base_url=public_url,
|
||||
workspace_id=os.getenv(
|
||||
"RECALL_WORKSPACE_ID", "33a035c7-03c1-4ec9-b752-18cdfbbefc42"
|
||||
).strip(),
|
||||
workspace_id=os.getenv("RECALL_WORKSPACE_ID", "").strip(),
|
||||
bot_name=os.getenv("RECALL_BOT_NAME", "Praison Meeting Agent").strip()
|
||||
or "Praison Meeting Agent",
|
||||
calendar_auto_record=auto_record not in {"0", "false", "no", "off"},
|
||||
File renamed without changes.
+7
-7
@@ -5,10 +5,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from integrations.recall.client import RecallAPIError, RecallClient
|
||||
from integrations.recall.config import RecallSettings
|
||||
from integrations.recall.store import RecallStore, webhook_event_key
|
||||
from integrations.recall.transcript import transcript_data_to_line, transcript_download_to_text
|
||||
from meeting_integrations.recall.client import RecallAPIError, RecallClient
|
||||
from meeting_integrations.recall.config import RecallSettings
|
||||
from meeting_integrations.recall.store import RecallStore, webhook_event_key
|
||||
from meeting_integrations.recall.transcript import transcript_data_to_line, transcript_download_to_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,7 +22,7 @@ def _speaker_prefix(line: str) -> str:
|
||||
def _upsert_live_transcript(meeting_id: str, line: str, *, partial: bool = False) -> str:
|
||||
"""Merge one live caption line; partial updates replace the last same-speaker line."""
|
||||
from praisonai_tools.tools.meeting_tools import get_meeting
|
||||
from integrations.recall.live import publish_live_chunk
|
||||
from meeting_integrations.recall.live import publish_live_chunk
|
||||
from pipeline import _metadata_lock
|
||||
|
||||
# Hold the merge lock across the read too: streaming transcript events are
|
||||
@@ -120,7 +120,7 @@ def process_recall_webhook(
|
||||
|
||||
if event_type == "bot.in_call_recording" and meeting_id:
|
||||
_merge_metadata(meeting_id, {"live_status": "live", "status": "live", "error": ""})
|
||||
from integrations.recall.live import set_live_status
|
||||
from meeting_integrations.recall.live import set_live_status
|
||||
|
||||
set_live_status(meeting_id, "live")
|
||||
return
|
||||
@@ -139,7 +139,7 @@ def process_recall_webhook(
|
||||
|
||||
if event_type == "bot.call_ended" and meeting_id:
|
||||
_merge_metadata(meeting_id, {"live_status": "ended"})
|
||||
from integrations.recall.live import set_live_status
|
||||
from meeting_integrations.recall.live import set_live_status
|
||||
|
||||
set_live_status(meeting_id, "ended")
|
||||
return
|
||||
+2
-2
@@ -6,8 +6,8 @@ import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from integrations.recall.client import RecallAPIError, RecallClient
|
||||
from integrations.recall.config import RecallConfigError, RecallSettings, load_recall_settings
|
||||
from meeting_integrations.recall.client import RecallAPIError, RecallClient
|
||||
from meeting_integrations.recall.config import RecallConfigError, RecallSettings, load_recall_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+14
-14
@@ -9,7 +9,7 @@ from contextlib import closing
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
|
||||
|
||||
def _state_db_path() -> str:
|
||||
@@ -100,42 +100,42 @@ class RecallStore:
|
||||
|
||||
@staticmethod
|
||||
def extract_bot_id(payload: dict[str, Any]) -> str | None:
|
||||
from integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
|
||||
return RecallClient.extract_bot_id(payload)
|
||||
|
||||
@staticmethod
|
||||
def extract_recording_id(payload: dict[str, Any]) -> str | None:
|
||||
from integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
|
||||
return RecallClient.extract_recording_id(payload)
|
||||
|
||||
@staticmethod
|
||||
def extract_transcript_id(payload: dict[str, Any]) -> str | None:
|
||||
from integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
|
||||
return RecallClient.extract_transcript_id(payload)
|
||||
|
||||
@staticmethod
|
||||
def mark_event_processed(event_key: str, event_type: str) -> bool:
|
||||
"""Return False if this event was already processed."""
|
||||
"""Claim an event for processing; return False if already processed.
|
||||
|
||||
Uses an atomic ``INSERT OR IGNORE`` so concurrent duplicate webhook
|
||||
deliveries (Svix retries/fan-out) cannot both pass the check — the
|
||||
second insert is ignored and reports zero changed rows.
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
with closing(_connect()) as conn, conn:
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM recall_webhook_events WHERE event_key = ?",
|
||||
(event_key,),
|
||||
).fetchone()
|
||||
if row:
|
||||
return False
|
||||
conn.execute(
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO recall_webhook_events (event_key, event_type, processed_at)
|
||||
INSERT OR IGNORE INTO recall_webhook_events
|
||||
(event_key, event_type, processed_at)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(event_key, event_type, datetime.now(timezone.utc).isoformat()),
|
||||
)
|
||||
return True
|
||||
return cursor.rowcount > 0
|
||||
|
||||
@staticmethod
|
||||
def save_calendar_setup(setup_id: str, platform: str, state: dict[str, Any]) -> None:
|
||||
File renamed without changes.
File renamed without changes.
@@ -11,18 +11,18 @@ from typing import Any
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, PlainTextResponse, Response, StreamingResponse
|
||||
|
||||
from integrations.recall.calendar import forward_calendar_callback
|
||||
from integrations.recall.calendar_service import (
|
||||
from meeting_integrations.recall.calendar import forward_calendar_callback
|
||||
from meeting_integrations.recall.calendar_service import (
|
||||
list_upcoming_calendar_meetings,
|
||||
process_calendar_webhook,
|
||||
sync_calendar_events,
|
||||
)
|
||||
from integrations.recall.config import RecallConfigError, load_recall_settings
|
||||
from integrations.recall.live import snapshot_for_api, wait_for_update
|
||||
from integrations.recall.processor import process_recall_webhook
|
||||
from integrations.recall.service import cancel_recall_bot, schedule_recall_bot
|
||||
from integrations.recall.store import RecallStore
|
||||
from integrations.recall.verify import VerificationError, verify_request_from_recall
|
||||
from meeting_integrations.recall.config import RecallConfigError, load_recall_settings
|
||||
from meeting_integrations.recall.live import snapshot_for_api, wait_for_update
|
||||
from meeting_integrations.recall.processor import process_recall_webhook
|
||||
from meeting_integrations.recall.service import cancel_recall_bot, schedule_recall_bot
|
||||
from meeting_integrations.recall.store import RecallStore
|
||||
from meeting_integrations.recall.verify import VerificationError, verify_request_from_recall
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -208,7 +208,7 @@ async def api_recall_calendar_status(_request: Request) -> JSONResponse:
|
||||
public_url = os.getenv("PUBLIC_API_BASE_URL", "").strip()
|
||||
calendars: list[dict[str, Any]] = []
|
||||
try:
|
||||
from integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
|
||||
settings = load_recall_settings()
|
||||
calendars = RecallClient(settings).list_calendars(status="connected")
|
||||
|
||||
@@ -45,9 +45,9 @@ def _duration_seconds(started_at: str | None, completed_at: str | None) -> int |
|
||||
|
||||
|
||||
def recover(meeting_id: str, bot_id: str | None = None) -> dict:
|
||||
from integrations.recall.client import RecallClient
|
||||
from integrations.recall.config import load_recall_settings
|
||||
from integrations.recall.transcript import transcript_download_to_text
|
||||
from meeting_integrations.recall.client import RecallClient
|
||||
from meeting_integrations.recall.config import load_recall_settings
|
||||
from meeting_integrations.recall.transcript import transcript_download_to_text
|
||||
from pipeline import merge_meeting_metadata, run_post_transcript_pipeline
|
||||
from praisonai_tools.tools.meeting_tools import get_meeting
|
||||
|
||||
|
||||
@@ -39,12 +39,12 @@ def run_smoke() -> None:
|
||||
os.environ.setdefault("RECALL_REGION", "eu-central-1")
|
||||
os.environ.setdefault("RECALL_API_KEY", "smoke-test-key")
|
||||
os.environ.setdefault("RECALL_WEBHOOK_VERIFICATION_SECRET", "whsec_" + base64.b64encode(b"smoke-secret-key-32-bytes-long!!").decode())
|
||||
os.environ.setdefault("RECALL_WORKSPACE_ID", "33a035c7-03c1-4ec9-b752-18cdfbbefc42")
|
||||
os.environ.setdefault("RECALL_WORKSPACE_ID", "smoke-workspace")
|
||||
os.environ.setdefault("PUBLIC_API_BASE_URL", "https://smoke.example.test")
|
||||
|
||||
import app as meeting_app # noqa: WPS433
|
||||
from integrations.recall.config import load_recall_settings
|
||||
from integrations.recall.verify import verify_request_from_recall
|
||||
from meeting_integrations.recall.config import load_recall_settings
|
||||
from meeting_integrations.recall.verify import verify_request_from_recall
|
||||
|
||||
settings = load_recall_settings()
|
||||
assert settings.api_key == "smoke-test-key"
|
||||
|
||||
@@ -462,6 +462,24 @@ async def _run_and_broadcast(
|
||||
collected_tool_calls: Dict[str, Dict] = {} # Merge STARTED+COMPLETED per tool_call_id
|
||||
collected_elements: List[Dict[str, Any]] = []
|
||||
|
||||
# Register this run so ``abort_run(run_id)`` can cancel it, and announce the
|
||||
# run to WS clients up-front. Without this the ``_active_runs`` registry was
|
||||
# never populated, so the Stop button and ``POST /api/chat/abort`` were
|
||||
# silent no-ops that always returned ``no_active_run`` (issue #274). The
|
||||
# ``run_started`` event also lets the frontend capture ``run_id`` and enable
|
||||
# the Stop button immediately, before the first token arrives.
|
||||
self_task = asyncio.current_task()
|
||||
if self_task is not None:
|
||||
mgr._active_runs[run_id] = self_task
|
||||
started_event: Dict[str, Any] = {
|
||||
"type": "run_started",
|
||||
"session_id": session_id,
|
||||
"run_id": run_id,
|
||||
}
|
||||
if agent_name:
|
||||
started_event["agent_name"] = agent_name
|
||||
await mgr.broadcast(session_id, started_event)
|
||||
|
||||
try:
|
||||
# Pass image attachments to provider for SDK native multimodal handling
|
||||
run_kwargs = {}
|
||||
@@ -644,6 +662,9 @@ async def _run_and_broadcast(
|
||||
"error": str(e),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
# Always deregister the run, on normal completion, error, or abort.
|
||||
mgr._active_runs.pop(run_id, None)
|
||||
|
||||
# Save assistant response
|
||||
if full_response or collected_elements:
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Tests for chat run abort / cancellation (issue #274).
|
||||
|
||||
Regression coverage for the wiring bug where ``ChatManager._active_runs`` was
|
||||
never populated, so ``abort_run`` (Stop button / ``POST /api/chat/abort`` /
|
||||
``chat_abort`` WS) always returned ``no_active_run`` even while a run was
|
||||
streaming.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from praisonaiui.provider import RunEvent, RunEventType
|
||||
|
||||
# ── Unit: abort_run against the _active_runs registry ────────────────
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_abort_cancels_active_run():
|
||||
from praisonaiui.features.chat import ChatManager
|
||||
|
||||
mgr = ChatManager()
|
||||
run_id = "test-run"
|
||||
|
||||
async def slow_run():
|
||||
await asyncio.sleep(60)
|
||||
|
||||
task = asyncio.create_task(slow_run())
|
||||
mgr._active_runs[run_id] = task
|
||||
|
||||
result = await mgr.abort_run(run_id)
|
||||
|
||||
assert result["status"] == "aborted"
|
||||
assert result["run_id"] == run_id
|
||||
# Give the event loop a tick to process the cancellation.
|
||||
await asyncio.sleep(0)
|
||||
assert task.cancelled() or task.done()
|
||||
# Registry entry removed on abort.
|
||||
assert run_id not in mgr._active_runs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_abort_unknown_run_returns_no_active_run():
|
||||
from praisonaiui.features.chat import ChatManager
|
||||
|
||||
mgr = ChatManager()
|
||||
result = await mgr.abort_run("does-not-exist")
|
||||
assert result["status"] == "no_active_run"
|
||||
assert result["run_id"] == "does-not-exist"
|
||||
|
||||
|
||||
# ── Integration: _run_and_broadcast registers + aborts cleanly ───────
|
||||
|
||||
|
||||
class _FakeWS:
|
||||
"""Collects broadcast frames pushed by ChatManager.broadcast."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.messages: list[dict] = []
|
||||
|
||||
async def send_text(self, data: str) -> None:
|
||||
import json
|
||||
|
||||
self.messages.append(json.loads(data))
|
||||
|
||||
|
||||
class _SlowProvider:
|
||||
"""Streams a couple of tokens then hangs, keeping the run active."""
|
||||
|
||||
async def run(self, content, *, session_id=None, agent_name=None, **kwargs):
|
||||
yield RunEvent(type=RunEventType.RUN_CONTENT, token="hello ")
|
||||
yield RunEvent(type=RunEventType.RUN_CONTENT, token="world")
|
||||
# Simulate a long generation so the run stays registered.
|
||||
await asyncio.sleep(60)
|
||||
yield RunEvent(type=RunEventType.RUN_COMPLETED, content="hello world")
|
||||
|
||||
|
||||
class _FakeDatastore:
|
||||
async def add_message(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_and_broadcast_registers_and_aborts(monkeypatch):
|
||||
import praisonaiui.server as server
|
||||
from praisonaiui.features.chat import ChatManager, _run_and_broadcast, set_chat_manager
|
||||
|
||||
mgr = ChatManager()
|
||||
set_chat_manager(mgr)
|
||||
ws = _FakeWS()
|
||||
mgr.add_ws_client("c1", ws)
|
||||
|
||||
monkeypatch.setattr(server, "get_provider", lambda: _SlowProvider())
|
||||
monkeypatch.setattr(server, "_datastore", _FakeDatastore())
|
||||
|
||||
task = asyncio.create_task(_run_and_broadcast("hi", "sess-1", "Agent", None))
|
||||
|
||||
# Wait until the run is registered (populated by _run_and_broadcast).
|
||||
for _ in range(100):
|
||||
if mgr._active_runs:
|
||||
break
|
||||
await asyncio.sleep(0.01)
|
||||
assert mgr._active_runs, "run was never registered in _active_runs"
|
||||
|
||||
run_id = next(iter(mgr._active_runs))
|
||||
|
||||
# Frontend contract: a run_started frame is broadcast with the run_id.
|
||||
started = [m for m in ws.messages if m.get("type") == "run_started"]
|
||||
assert started and started[0]["run_id"] == run_id
|
||||
|
||||
# Abort the active run.
|
||||
result = await mgr.abort_run(run_id)
|
||||
assert result["status"] == "aborted"
|
||||
|
||||
# Let the cancellation propagate and _run_and_broadcast finish.
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
# Clients receive a run_cancelled frame, and the registry is cleaned up.
|
||||
cancelled = [m for m in ws.messages if m.get("type") == "run_cancelled"]
|
||||
assert cancelled and cancelled[0]["run_id"] == run_id
|
||||
assert run_id not in mgr._active_runs
|
||||
assert task.done()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_and_broadcast_cleans_up_on_normal_completion(monkeypatch):
|
||||
import praisonaiui.server as server
|
||||
from praisonaiui.features.chat import ChatManager, _run_and_broadcast, set_chat_manager
|
||||
|
||||
class _QuickProvider:
|
||||
async def run(self, content, *, session_id=None, agent_name=None, **kwargs):
|
||||
yield RunEvent(type=RunEventType.RUN_CONTENT, token="done")
|
||||
yield RunEvent(type=RunEventType.RUN_COMPLETED, content="done")
|
||||
|
||||
mgr = ChatManager()
|
||||
set_chat_manager(mgr)
|
||||
monkeypatch.setattr(server, "get_provider", lambda: _QuickProvider())
|
||||
monkeypatch.setattr(server, "_datastore", _FakeDatastore())
|
||||
|
||||
await _run_and_broadcast("hi", "sess-2", None, None)
|
||||
|
||||
# No leftover registry entries after a normal run.
|
||||
assert mgr._active_runs == {}
|
||||
@@ -27,7 +27,7 @@ def part_b_env(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("RECALL_REGION", "eu-central-1")
|
||||
monkeypatch.setenv("RECALL_API_KEY", "test-recall-key")
|
||||
monkeypatch.setenv("RECALL_WEBHOOK_VERIFICATION_SECRET", "whsec_test")
|
||||
monkeypatch.setenv("RECALL_WORKSPACE_ID", "33a035c7-03c1-4ec9-b752-18cdfbbefc42")
|
||||
monkeypatch.setenv("RECALL_WORKSPACE_ID", "test-workspace")
|
||||
monkeypatch.setenv("PUBLIC_API_BASE_URL", "https://dev.example.test")
|
||||
monkeypatch.setenv("PRAISONAI_MEETINGS_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("PRAISONAI_MEETINGS_DB", str(tmp_path / "meetings.db"))
|
||||
@@ -35,7 +35,7 @@ def part_b_env(monkeypatch, tmp_path):
|
||||
|
||||
|
||||
def test_transcript_data_to_line():
|
||||
tx = _load("part_b_transcript", _ROOT / "integrations" / "recall" / "transcript.py")
|
||||
tx = _load("part_b_transcript", _ROOT / "meeting_integrations" / "recall" / "transcript.py")
|
||||
payload = {
|
||||
"data": {
|
||||
"data": {
|
||||
@@ -48,7 +48,7 @@ def test_transcript_data_to_line():
|
||||
|
||||
|
||||
def test_live_hub_publish_and_snapshot():
|
||||
live = _load("part_b_live", _ROOT / "integrations" / "recall" / "live.py")
|
||||
live = _load("part_b_live", _ROOT / "meeting_integrations" / "recall" / "live.py")
|
||||
live.publish_live_chunk("m-live", line="Alice: Hi", full_text="Alice: Hi", live_status="live")
|
||||
snap = live.snapshot_for_api("m-live", {"metadata": {}})
|
||||
assert snap["transcript"] == "Alice: Hi"
|
||||
@@ -56,8 +56,8 @@ def test_live_hub_publish_and_snapshot():
|
||||
|
||||
|
||||
def test_create_bot_includes_realtime_config():
|
||||
client_mod = _load("part_b_client", _ROOT / "integrations" / "recall" / "client.py")
|
||||
config_mod = _load("part_b_config", _ROOT / "integrations" / "recall" / "config.py")
|
||||
client_mod = _load("part_b_client", _ROOT / "meeting_integrations" / "recall" / "client.py")
|
||||
config_mod = _load("part_b_config", _ROOT / "meeting_integrations" / "recall" / "config.py")
|
||||
settings = config_mod.load_recall_settings()
|
||||
client = client_mod.RecallClient(settings)
|
||||
client._request = MagicMock(return_value={"id": "bot-1"}) # noqa: SLF001
|
||||
@@ -76,7 +76,7 @@ def test_create_bot_includes_realtime_config():
|
||||
|
||||
|
||||
def test_calendar_eligibility():
|
||||
cal = _load("part_b_calendar", _ROOT / "integrations" / "recall" / "calendar_service.py")
|
||||
cal = _load("part_b_calendar", _ROOT / "meeting_integrations" / "recall" / "calendar_service.py")
|
||||
assert cal.is_eligible_for_auto_record(
|
||||
{
|
||||
"is_deleted": False,
|
||||
@@ -88,7 +88,7 @@ def test_calendar_eligibility():
|
||||
|
||||
|
||||
def test_transcript_webhook_keys_are_unique_per_utterance():
|
||||
store = _load("part_b_store", _ROOT / "integrations" / "recall" / "store.py")
|
||||
store = _load("part_b_store", _ROOT / "meeting_integrations" / "recall" / "store.py")
|
||||
bot = "bot-1"
|
||||
p1 = {
|
||||
"data": {
|
||||
@@ -109,8 +109,8 @@ def test_transcript_webhook_keys_are_unique_per_utterance():
|
||||
|
||||
def test_upsert_live_transcript_replaces_partial():
|
||||
pytest.importorskip("praisonai_tools") # external PraisonAI-Tools dep, optional in CI
|
||||
processor = _load("part_b_processor2", _ROOT / "integrations" / "recall" / "processor.py")
|
||||
with patch("integrations.recall.live.publish_live_chunk") as publish:
|
||||
processor = _load("part_b_processor2", _ROOT / "meeting_integrations" / "recall" / "processor.py")
|
||||
with patch("meeting_integrations.recall.live.publish_live_chunk") as publish:
|
||||
with patch("praisonai_tools.tools.meeting_tools.get_meeting") as get_m:
|
||||
get_m.__wrapped__ = MagicMock(
|
||||
return_value={"metadata": {"transcript": "Alice: hel"}}
|
||||
@@ -121,8 +121,8 @@ def test_upsert_live_transcript_replaces_partial():
|
||||
|
||||
|
||||
def test_process_transcript_data_webhook():
|
||||
processor = _load("part_b_processor", _ROOT / "integrations" / "recall" / "processor.py")
|
||||
config_mod = _load("part_b_config2", _ROOT / "integrations" / "recall" / "config.py")
|
||||
processor = _load("part_b_processor", _ROOT / "meeting_integrations" / "recall" / "processor.py")
|
||||
config_mod = _load("part_b_config2", _ROOT / "meeting_integrations" / "recall" / "config.py")
|
||||
settings = config_mod.load_recall_settings()
|
||||
|
||||
with patch.object(processor, "_append_live_transcript") as append:
|
||||
|
||||
@@ -9,8 +9,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
# The ingest pipeline calls the external PraisonAI-Tools ``meeting_tools`` at
|
||||
# runtime; it is not a PraisonAIUI CI dependency, so skip when unavailable.
|
||||
# Meeting tools live in the external PraisonAI-Tools package; skip when unavailable in CI.
|
||||
pytest.importorskip("praisonai_tools")
|
||||
|
||||
_PIPELINE = (
|
||||
|
||||
@@ -32,7 +32,7 @@ def recall_env(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("RECALL_REGION", "eu-central-1")
|
||||
monkeypatch.setenv("RECALL_API_KEY", "test-recall-key")
|
||||
monkeypatch.setenv("RECALL_WEBHOOK_VERIFICATION_SECRET", _SECRET)
|
||||
monkeypatch.setenv("RECALL_WORKSPACE_ID", "33a035c7-03c1-4ec9-b752-18cdfbbefc42")
|
||||
monkeypatch.setenv("RECALL_WORKSPACE_ID", "test-workspace")
|
||||
monkeypatch.setenv("PUBLIC_API_BASE_URL", "https://dev.example.test")
|
||||
monkeypatch.setenv("PRAISONAI_MEETINGS_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("PRAISONAI_MEETINGS_DB", str(tmp_path / "meetings.db"))
|
||||
@@ -40,7 +40,7 @@ def recall_env(monkeypatch, tmp_path):
|
||||
|
||||
|
||||
def test_verify_request_accepts_valid_signature():
|
||||
verify_mod = _load("recall_verify", _ROOT / "integrations" / "recall" / "verify.py")
|
||||
verify_mod = _load("recall_verify", _ROOT / "meeting_integrations" / "recall" / "verify.py")
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_verify_request_accepts_valid_signature():
|
||||
|
||||
|
||||
def test_verify_request_rejects_bad_signature():
|
||||
verify_mod = _load("recall_verify", _ROOT / "integrations" / "recall" / "verify.py")
|
||||
verify_mod = _load("recall_verify", _ROOT / "meeting_integrations" / "recall" / "verify.py")
|
||||
with pytest.raises(verify_mod.VerificationError):
|
||||
verify_mod.verify_request_from_recall(
|
||||
secret=_SECRET,
|
||||
@@ -78,7 +78,7 @@ def test_verify_request_rejects_bad_signature():
|
||||
|
||||
def test_schedule_bot_persists_intent_before_api():
|
||||
pytest.importorskip("praisonai_tools") # external PraisonAI-Tools dep, optional in CI
|
||||
service = _load("recall_service", _ROOT / "integrations" / "recall" / "service.py")
|
||||
service = _load("recall_service", _ROOT / "meeting_integrations" / "recall" / "service.py")
|
||||
client = MagicMock()
|
||||
client.create_bot.return_value = {"id": "bot-abc"}
|
||||
|
||||
@@ -104,7 +104,7 @@ def test_webhook_rejects_invalid_signature():
|
||||
|
||||
def test_webhook_accepts_valid_signature_and_queues():
|
||||
example = _load("meeting_agent_recall_app2", _ROOT / "app.py")
|
||||
verify_mod = _load("recall_verify2", _ROOT / "integrations" / "recall" / "verify.py")
|
||||
verify_mod = _load("recall_verify2", _ROOT / "meeting_integrations" / "recall" / "verify.py")
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
@@ -136,7 +136,7 @@ def test_webhook_accepts_valid_signature_and_queues():
|
||||
|
||||
|
||||
def test_transcript_download_to_text():
|
||||
tx = _load("recall_transcript", _ROOT / "integrations" / "recall" / "transcript.py")
|
||||
tx = _load("recall_transcript", _ROOT / "meeting_integrations" / "recall" / "transcript.py")
|
||||
data = [
|
||||
{"speaker": "Alice", "words": [{"text": "Hello"}, {"text": "team"}]},
|
||||
{"speaker": "Bob", "text": "Hi there"},
|
||||
|
||||
Reference in new issue
Block a user