"""Dify chat applications exposed as a Pipecat LLM processor.""" from __future__ import annotations from uuid import uuid4 from dify_client import AsyncClient, models from loguru import logger from models import AssistantConfig from pipecat.frames.frames import ( Frame, LLMContextFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, LLMTextFrame, ) from pipecat.processors.frame_processor import FrameDirection from pipecat.services.llm_service import LLMService from pipecat.services.settings import LLMSettings def normalize_api_base(url: str) -> str: """Accept a Dify host, /v1 base URL, or full chat endpoint.""" base = (url or "https://api.dify.ai").strip().rstrip("/") if base.endswith("/chat-messages"): base = base[: -len("/chat-messages")] if not base.endswith("/v1"): base = f"{base}/v1" return base def last_user_text(messages: list[dict]) -> str: for message in reversed(messages or []): if message.get("role") != "user": continue content = message.get("content") if isinstance(content, str): return content if isinstance(content, list): return "".join( str(part.get("text") or "") for part in content if isinstance(part, dict) ) return "" class DifyLLMService(LLMService): """Stream Dify answer events into Pipecat's standard text frames.""" def __init__( self, cfg: AssistantConfig, *, client: AsyncClient | None = None, user_id: str | None = None, ): super().__init__( settings=LLMSettings( model=None, system_instruction=None, temperature=None, max_tokens=None, top_p=None, top_k=None, frequency_penalty=None, presence_penalty=None, seed=None, filter_incomplete_user_turns=None, user_turn_completion_config=None, ) ) self._client = client or AsyncClient( api_key=cfg.dify_api_key, api_base=normalize_api_base(cfg.dify_api_url), ) self._user_id = user_id or f"ai-video-{uuid4().hex}" self._conversation_id = "" async def process_frame(self, frame: Frame, direction: FrameDirection): await super().process_frame(frame, direction) if not isinstance(frame, LLMContextFrame): await self.push_frame(frame, direction) return user_text = last_user_text(frame.context.get_messages()) if not user_text: return await self.push_frame(LLMFullResponseStartFrame()) try: request = models.ChatRequest( query=user_text, inputs={}, user=self._user_id, response_mode=models.ResponseMode.STREAMING, conversation_id=self._conversation_id, auto_generate_name=False, ) events = await self._client.achat_messages(request, timeout=120.0) async for event in events: conversation_id = getattr(event, "conversation_id", "") if conversation_id: self._conversation_id = conversation_id event_name = str(getattr(event, "event", "")) if event_name == "error": logger.error( "Dify 流式错误: " f"code={getattr(event, 'code', '')} " f"message={getattr(event, 'message', '')}" ) continue text = ( getattr(event, "answer", "") if event_name in {"message", "agent_message"} else "" ) if event_name == "text_chunk": text = getattr(getattr(event, "data", None), "text", "") if text: await self.push_frame(LLMTextFrame(text)) except Exception as exc: # noqa: BLE001 - one failed turn must not kill the call logger.error(f"Dify 调用失败: {exc}") finally: await self.push_frame(LLMFullResponseEndFrame())