Files
ai-video-fullstack/backend/services/brains/dify_llm.py
Xin Wang 00270a5c01 Add Dify integration and enhance workflow node specifications
- 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.
2026-07-11 22:26:31 +08:00

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())