- Introduce new fields `dify_api_url` and `dify_api_key` in `AssistantConfig` for Dify API integration. - Update `requirements.txt` to include `dify-client-python` for Dify SDK support. - Modify `config_resolver` to handle Dify connection information. - Add a new `globalNode` type in workflow specifications to provide unified settings across workflows. - Enhance node specifications with additional constraints and default values for better configuration management. - Update frontend components to support the new `globalNode` type and its properties, improving workflow editor functionality.
128 lines
4.3 KiB
Python
128 lines
4.3 KiB
Python
"""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())
|