mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1de8d054a8 | ||
|
|
a739ba8a6c | ||
|
|
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"
|
||||
|
||||
@@ -315,6 +315,13 @@ class ChannelsFeature(BaseFeatureProtocol):
|
||||
|
||||
Returns None on success, or an error string on failure.
|
||||
"""
|
||||
# Idempotent start (#277): stop any bot already running for this channel
|
||||
# before creating a new one. Without this, a toggle off/on, a re-add, or
|
||||
# an autostart+manual race leaves a duplicate bot with duplicate event
|
||||
# handlers attached while _live_bots only tracks the latest task.
|
||||
if channel_id in _live_bots:
|
||||
await self._stop_channel_bot(channel_id)
|
||||
|
||||
platform = entry["platform"]
|
||||
config = entry.get("config", {})
|
||||
fallback_errors: List[str] = []
|
||||
@@ -905,9 +912,15 @@ class ChannelsFeature(BaseFeatureProtocol):
|
||||
try:
|
||||
if hasattr(bot, "stop"):
|
||||
await bot.stop()
|
||||
# Cancel the task if still running
|
||||
# Cancel the task and await it so the old bot is fully torn down
|
||||
# before this returns — otherwise a restart (#277) can spawn a new
|
||||
# bot while the previous task is still cancelling (a zombie).
|
||||
if info and info.get("task") and not info["task"].done():
|
||||
info["task"].cancel()
|
||||
try:
|
||||
await info["task"]
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info(f"Stopped bot for channel '{channel_id}'")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping bot for '{channel_id}': {e}")
|
||||
@@ -918,6 +931,19 @@ class ChannelsFeature(BaseFeatureProtocol):
|
||||
gw = self._get_gateway()
|
||||
if gw is not None:
|
||||
getattr(gw, "_channel_bots", {}).pop(channel_id, None)
|
||||
# Mirror start-time registration: drop the task from the gateway
|
||||
# too, so a restart (#277) leaves no stale/cancelled task behind.
|
||||
# The dict form is overwritten on restart, but the list form would
|
||||
# otherwise accumulate a cancelled task on every restart.
|
||||
channel_tasks = getattr(gw, "_channel_tasks", None)
|
||||
task = info.get("task") if info else None
|
||||
if isinstance(channel_tasks, dict):
|
||||
channel_tasks.pop(channel_id, None)
|
||||
elif isinstance(channel_tasks, list) and task is not None:
|
||||
try:
|
||||
channel_tasks.remove(task)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Update local state
|
||||
ch = _channels.get(channel_id)
|
||||
|
||||
@@ -144,6 +144,74 @@ async def test_auto_start_skips_disabled_channels():
|
||||
assert mock_start.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_channel_bot_is_idempotent_stops_existing_first():
|
||||
"""Regression for #277: starting a bot for a channel that already has a
|
||||
live bot must stop the existing one first so no duplicate bot/task lingers.
|
||||
"""
|
||||
feature = ChannelsFeature()
|
||||
gw = _GatewayDictTasks()
|
||||
entry = {"id": "ch6", "platform": "discord", "config": {"bot_token": "x" * 30}}
|
||||
|
||||
with patch.object(feature, "_get_gateway", return_value=gw), patch.object(
|
||||
feature, "_create_bot_direct", side_effect=lambda *a, **k: _FakeBot()
|
||||
), patch.object(feature, "_attach_chat_bridge"):
|
||||
await feature._start_channel_bot("ch6", entry)
|
||||
first = channels_mod._live_bots["ch6"]
|
||||
|
||||
await feature._start_channel_bot("ch6", entry)
|
||||
second = channels_mod._live_bots["ch6"]
|
||||
|
||||
# A new bot/task replaced the first, and only one entry is tracked.
|
||||
assert second is not first
|
||||
assert first["bot"] is not second["bot"]
|
||||
assert list(channels_mod._live_bots.keys()).count("ch6") == 1
|
||||
# The first task was cancelled by the pre-start _stop_channel_bot guard.
|
||||
assert first["task"].cancelled() or first["task"].done()
|
||||
# Gateway has exactly one (fresh) bot reference — no stale entry.
|
||||
assert gw._channel_bots["ch6"] is second["bot"]
|
||||
# ...and exactly one (fresh) task reference in the dict registry.
|
||||
assert gw._channel_tasks["ch6"] is second["task"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_leaves_no_stale_task_in_list_gateway():
|
||||
"""Regression for #277: with a list-based gateway _channel_tasks, a restart
|
||||
must not accumulate the old cancelled task alongside the fresh one.
|
||||
"""
|
||||
feature = ChannelsFeature()
|
||||
gw = _GatewayListTasks()
|
||||
entry = {"id": "ch8", "platform": "discord", "config": {"bot_token": "x" * 30}}
|
||||
|
||||
with patch.object(feature, "_get_gateway", return_value=gw), patch.object(
|
||||
feature, "_create_bot_direct", side_effect=lambda *a, **k: _FakeBot()
|
||||
), patch.object(feature, "_attach_chat_bridge"):
|
||||
await feature._start_channel_bot("ch8", entry)
|
||||
await feature._start_channel_bot("ch8", entry)
|
||||
current = channels_mod._live_bots["ch8"]
|
||||
|
||||
# Only the fresh task remains in the list — the cancelled one was removed.
|
||||
assert gw._channel_tasks == [current["task"]]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_channel_bot_no_existing_does_not_stop():
|
||||
"""Guard must be a no-op on first start (nothing in _live_bots)."""
|
||||
feature = ChannelsFeature()
|
||||
gw = _GatewayDictTasks()
|
||||
entry = {"id": "ch7", "platform": "discord", "config": {"bot_token": "x" * 30}}
|
||||
|
||||
with patch.object(feature, "_get_gateway", return_value=gw), patch.object(
|
||||
feature, "_create_bot_direct", return_value=_FakeBot()
|
||||
), patch.object(feature, "_attach_chat_bridge"), patch.object(
|
||||
feature, "_stop_channel_bot"
|
||||
) as mock_stop:
|
||||
error = await feature._start_channel_bot("ch7", entry)
|
||||
|
||||
assert error is None
|
||||
mock_stop.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auto_start_failure_sets_not_running_and_error():
|
||||
feature = ChannelsFeature()
|
||||
|
||||
@@ -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