mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5168ea253a | ||
|
|
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"
|
||||
|
||||
@@ -354,6 +354,13 @@ class MCPClientManager:
|
||||
connection_data=server_config,
|
||||
)
|
||||
|
||||
# Disconnect any existing client for this name (reconnect) before
|
||||
# replacing it, otherwise the previous stdio subprocess/streams are
|
||||
# orphaned when the reference is overwritten.
|
||||
previous_client = self._clients.pop(name, None)
|
||||
if previous_client is not None:
|
||||
await self._safe_disconnect(name, previous_client)
|
||||
|
||||
# Register server
|
||||
_mcp_servers[name] = server
|
||||
self._clients[name] = client
|
||||
@@ -374,16 +381,33 @@ class MCPClientManager:
|
||||
else:
|
||||
server.status = MCPStatus.ERROR
|
||||
server.last_error = "Connection failed"
|
||||
await self._cleanup_failed_client(name, client)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to connect MCP server {name}")
|
||||
server.status = MCPStatus.ERROR
|
||||
server.last_error = str(e)
|
||||
await self._cleanup_failed_client(name, client)
|
||||
|
||||
# Notify final status
|
||||
await self._notify_status_change(server)
|
||||
return server
|
||||
|
||||
async def _safe_disconnect(self, name: str, client: MCPClientProtocol) -> None:
|
||||
"""Disconnect a client, logging (not raising) on failure."""
|
||||
try:
|
||||
await client.disconnect()
|
||||
except Exception:
|
||||
logger.exception(f"Error disconnecting MCP client {name}")
|
||||
|
||||
async def _cleanup_failed_client(self, name: str, client: MCPClientProtocol) -> None:
|
||||
"""Release a client whose connection failed and drop it from the registry."""
|
||||
await self._safe_disconnect(name, client)
|
||||
# Only remove if this client is still the registered one (avoid evicting
|
||||
# a newer client from a concurrent reconnect).
|
||||
if self._clients.get(name) is client:
|
||||
del self._clients[name]
|
||||
|
||||
async def disconnect_server(
|
||||
self, name: str, session_context: Optional[Dict[str, Any]] = None
|
||||
) -> bool:
|
||||
|
||||
@@ -347,6 +347,61 @@ class TestMCPClientManager:
|
||||
with pytest.raises(ValueError, match="Invalid MCP server config"):
|
||||
await manager.connect_server(config)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconnect_same_name_disconnects_old(self):
|
||||
"""Reconnecting the same name disconnects the previous client (no leak)."""
|
||||
manager = MCPClientManager()
|
||||
|
||||
old_client = AsyncMock()
|
||||
old_client.disconnect = AsyncMock()
|
||||
manager._clients["srv"] = old_client
|
||||
|
||||
config = {"name": "srv", "command": "echo", "args": []}
|
||||
with patch("praisonaiui.features.mcp.StdioMCPClient") as mock_client_class:
|
||||
new_client = AsyncMock()
|
||||
new_client.connect.return_value = True
|
||||
new_client.list_tools.return_value = []
|
||||
mock_client_class.return_value = new_client
|
||||
|
||||
await manager.connect_server(config)
|
||||
|
||||
old_client.disconnect.assert_awaited_once()
|
||||
assert manager._clients["srv"] is new_client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_failure_removes_client(self):
|
||||
"""A failed connect (returns False) removes the client from the registry."""
|
||||
manager = MCPClientManager()
|
||||
config = {"name": "srv", "command": "echo", "args": []}
|
||||
|
||||
with patch("praisonaiui.features.mcp.StdioMCPClient") as mock_client_class:
|
||||
client = AsyncMock()
|
||||
client.connect.return_value = False
|
||||
mock_client_class.return_value = client
|
||||
|
||||
server = await manager.connect_server(config)
|
||||
|
||||
assert server.status == MCPStatus.ERROR
|
||||
client.disconnect.assert_awaited_once()
|
||||
assert "srv" not in manager._clients
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_exception_removes_client(self):
|
||||
"""A connect that raises removes the client from the registry."""
|
||||
manager = MCPClientManager()
|
||||
config = {"name": "srv", "command": "echo", "args": []}
|
||||
|
||||
with patch("praisonaiui.features.mcp.StdioMCPClient") as mock_client_class:
|
||||
client = AsyncMock()
|
||||
client.connect.side_effect = Exception("boom")
|
||||
mock_client_class.return_value = client
|
||||
|
||||
server = await manager.connect_server(config)
|
||||
|
||||
assert server.status == MCPStatus.ERROR
|
||||
client.disconnect.assert_awaited_once()
|
||||
assert "srv" not in manager._clients
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_server_success(self):
|
||||
"""Test successful server disconnect."""
|
||||
|
||||
@@ -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